Merge remote-tracking branch 'upstream/master' into anchor-part3
# Conflicts: # Tests/test_imagefontctl.py
|  | @ -33,7 +33,8 @@ pip install pyroma | |||
| pip install test-image-results | ||||
| pip install numpy | ||||
| 
 | ||||
| # TODO Remove when 3.9 includes setuptools 49.3.2+: | ||||
| # TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: | ||||
| if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then pip install -U "setuptools>=49.3.2" ; fi | ||||
| if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then pip install -U "setuptools>=49.3.2" ; fi | ||||
| 
 | ||||
| if [[ $TRAVIS_PYTHON_VERSION == 3.* ]]; then | ||||
|  |  | |||
							
								
								
									
										8
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -63,9 +63,9 @@ jobs: | |||
|     - name: pip install wheel pytest pytest-cov | ||||
|       run: python -m pip install wheel pytest pytest-cov | ||||
| 
 | ||||
|     # TODO Remove when 3.9 includes setuptools 49.3.2+: | ||||
|     # TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: | ||||
|     - name: Upgrade setuptools | ||||
|       if: "contains(matrix.python-version, '3.9')" | ||||
|       if: "contains(matrix.python-version, '3.8') || contains(matrix.python-version, '3.9')" | ||||
|       run: python -m pip install -U "setuptools>=49.3.2" | ||||
| 
 | ||||
|     - name: Install dependencies | ||||
|  | @ -105,6 +105,10 @@ jobs: | |||
|     - name: Build dependencies / WebP | ||||
|       if: steps.build-cache.outputs.cache-hit != 'true' | ||||
|       run: "& winbuild\\build\\build_dep_libwebp.cmd" | ||||
|     # for FreeType CBDT font support | ||||
|     - name: Build dependencies / libpng | ||||
|       if: steps.build-cache.outputs.cache-hit != 'true' | ||||
|       run: "& winbuild\\build\\build_dep_libpng.cmd" | ||||
|     - name: Build dependencies / FreeType | ||||
|       if: steps.build-cache.outputs.cache-hit != 'true' | ||||
|       run: "& winbuild\\build\\build_dep_freetype.cmd" | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/BungeeColor-Regular_colr_Windows.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/DejaVuSans-24-1-stripped.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/DejaVuSans-24-2-stripped.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/DejaVuSans-24-4-stripped.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/DejaVuSans-24-8-stripped.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -2,15 +2,20 @@ | |||
| NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts | ||||
| NotoSans-Regular.ttf, from https://www.google.com/get/noto/ | ||||
| NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ | ||||
| NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji | ||||
| AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype | ||||
| TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny | ||||
| ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa | ||||
| ter-x20b.pcf, from http://terminus-font.sourceforge.net/ | ||||
| BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee | ||||
| 
 | ||||
| All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. | ||||
| 
 | ||||
| OpenSansCondensed-LightItalic.tt, from https://fonts.google.com/specimen/Open+Sans, under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) | ||||
| 
 | ||||
| DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. | ||||
| 
 | ||||
| 
 | ||||
| 10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base | ||||
| 
 | ||||
| "Public domain font.  Share and enjoy." | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/NotoColorEmoji.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -11,6 +11,7 @@ import tempfile | |||
| from io import BytesIO | ||||
| 
 | ||||
| import pytest | ||||
| from packaging.version import parse as parse_version | ||||
| 
 | ||||
| from PIL import Image, ImageMath, features | ||||
| 
 | ||||
|  | @ -162,6 +163,16 @@ def skip_unless_feature(feature): | |||
|     return pytest.mark.skipif(not features.check(feature), reason=reason) | ||||
| 
 | ||||
| 
 | ||||
| def skip_unless_feature_version(feature, version_required, reason=None): | ||||
|     if not features.check(feature): | ||||
|         return pytest.mark.skip(f"{feature} not available") | ||||
|     if reason is None: | ||||
|         reason = f"{feature} is older than {version_required}" | ||||
|     version_required = parse_version(version_required) | ||||
|     version_available = parse_version(features.version(feature)) | ||||
|     return pytest.mark.skipif(version_available < version_required, reason=reason) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") | ||||
| class PillowLeakTestCase: | ||||
|     # requires unix/macOS | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bitmap_font_1_basic.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 481 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bitmap_font_1_raqm.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 480 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bitmap_font_2_basic.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 661 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bitmap_font_2_raqm.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 658 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bitmap_font_4_basic.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bitmap_font_4_raqm.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bitmap_font_8_basic.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bitmap_font_8_raqm.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/cbdt_notocoloremoji.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/cbdt_notocoloremoji_mask.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/colr_bungee.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/colr_bungee_mask.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 284 B After Width: | Height: | Size: 248 B | 
| Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 199 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_arc_high.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 384 B After Width: | Height: | Size: 314 B | 
| Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 428 B | 
| Before Width: | Height: | Size: 438 B After Width: | Height: | Size: 426 B | 
| Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 430 B | 
| Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 404 B | 
| Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 237 B | 
| Before Width: | Height: | Size: 324 B After Width: | Height: | Size: 301 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_chord_too_fat.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 519 B | 
| Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 493 B | 
| Before Width: | Height: | Size: 507 B After Width: | Height: | Size: 514 B | 
| Before Width: | Height: | Size: 427 B After Width: | Height: | Size: 429 B | 
| Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 318 B | 
| Before Width: | Height: | Size: 466 B After Width: | Height: | Size: 408 B | 
| Before Width: | Height: | Size: 602 B After Width: | Height: | Size: 622 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_ellipse_various_sizes.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_ellipse_various_sizes_filled.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
| Before Width: | Height: | Size: 452 B After Width: | Height: | Size: 439 B | 
| Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 465 B | 
| Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.3 KiB | 
| Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 333 B | 
| Before Width: | Height: | Size: 259 B After Width: | Height: | Size: 267 B | 
| Before Width: | Height: | Size: 394 B After Width: | Height: | Size: 375 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_pieslice_wide.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 496 B After Width: | Height: | Size: 493 B | 
| Before Width: | Height: | Size: 523 B After Width: | Height: | Size: 519 B | 
| Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 405 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/standard_embedded.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.8 KiB | 
|  | @ -60,6 +60,10 @@ class TestDecompressionBomb: | |||
|         with pytest.raises(Image.DecompressionBombError): | ||||
|             Image.open("Tests/images/decompression_bomb.gif") | ||||
| 
 | ||||
|     def test_exception_bmp(self): | ||||
|         with pytest.raises(Image.DecompressionBombError): | ||||
|             Image.open("Tests/images/bmp/b/reallybig.bmp") | ||||
| 
 | ||||
| 
 | ||||
| class TestDecompressionCrop: | ||||
|     @classmethod | ||||
|  |  | |||
|  | @ -498,6 +498,12 @@ class TestImage: | |||
|         with pytest.raises(ValueError): | ||||
|             Image.core.fill("RGB", (2, -2), (0, 0, 0)) | ||||
| 
 | ||||
|     def test_one_item_tuple(self): | ||||
|         for mode in ("I", "F", "L"): | ||||
|             im = Image.new(mode, (100, 100), (5,)) | ||||
|             px = im.load() | ||||
|             assert px[0, 0] == 5 | ||||
| 
 | ||||
|     def test_linear_gradient_wrong_mode(self): | ||||
|         # Arrange | ||||
|         wrong_mode = "RGB" | ||||
|  |  | |||
|  | @ -330,21 +330,22 @@ class TestCffi(AccessTest): | |||
| class TestImagePutPixelError(AccessTest): | ||||
|     IMAGE_MODES1 = ["L", "LA", "RGB", "RGBA"] | ||||
|     IMAGE_MODES2 = ["I", "I;16", "BGR;15"] | ||||
|     INVALID_TYPES1 = ["foo", 1.0, None] | ||||
|     INVALID_TYPES2 = [*INVALID_TYPES1, (10,)] | ||||
|     INVALID_TYPES = ["foo", 1.0, None] | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", IMAGE_MODES1) | ||||
|     def test_putpixel_type_error1(self, mode): | ||||
|         im = hopper(mode) | ||||
|         for v in self.INVALID_TYPES1: | ||||
|         for v in self.INVALID_TYPES: | ||||
|             with pytest.raises(TypeError, match="color must be int or tuple"): | ||||
|                 im.putpixel((0, 0), v) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", IMAGE_MODES2) | ||||
|     def test_putpixel_type_error2(self, mode): | ||||
|         im = hopper(mode) | ||||
|         for v in self.INVALID_TYPES2: | ||||
|             with pytest.raises(TypeError, match="color must be int"): | ||||
|         for v in self.INVALID_TYPES: | ||||
|             with pytest.raises( | ||||
|                 TypeError, match="color must be int or single-element tuple" | ||||
|             ): | ||||
|                 im.putpixel((0, 0), v) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) | ||||
|  |  | |||
|  | @ -311,8 +311,8 @@ def test_subtract(): | |||
| 
 | ||||
|     # Assert | ||||
|     assert new.getbbox() == (25, 50, 76, 76) | ||||
|     assert new.getpixel((50, 50)) == GREEN | ||||
|     assert new.getpixel((50, 51)) == BLACK | ||||
|     assert new.getpixel((50, 51)) == GREEN | ||||
|     assert new.getpixel((50, 52)) == BLACK | ||||
| 
 | ||||
| 
 | ||||
| def test_subtract_scale_offset(): | ||||
|  | @ -350,8 +350,8 @@ def test_subtract_modulo(): | |||
| 
 | ||||
|     # Assert | ||||
|     assert new.getbbox() == (25, 50, 76, 76) | ||||
|     assert new.getpixel((50, 50)) == GREEN | ||||
|     assert new.getpixel((50, 51)) == BLACK | ||||
|     assert new.getpixel((50, 51)) == GREEN | ||||
|     assert new.getpixel((50, 52)) == BLACK | ||||
| 
 | ||||
| 
 | ||||
| def test_subtract_modulo_no_clip(): | ||||
|  |  | |||
|  | @ -164,6 +164,19 @@ def test_arc_width_non_whole_angle(): | |||
|     assert_image_similar_tofile(im, expected, 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_arc_high(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (200, 200)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|     # Act | ||||
|     draw.arc([10, 10, 89, 189], 20, 330, width=20, fill="white") | ||||
|     draw.arc([110, 10, 189, 189], 20, 150, width=20, fill="white") | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_equal(im, Image.open("Tests/images/imagedraw_arc_high.png")) | ||||
| 
 | ||||
| 
 | ||||
| def test_bitmap(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|  | @ -194,13 +207,11 @@ def helper_chord(mode, bbox, start, end): | |||
| def test_chord1(): | ||||
|     for mode in ["RGB", "L"]: | ||||
|         helper_chord(mode, BBOX1, 0, 180) | ||||
|         helper_chord(mode, BBOX1, 0.5, 180.4) | ||||
| 
 | ||||
| 
 | ||||
| def test_chord2(): | ||||
|     for mode in ["RGB", "L"]: | ||||
|         helper_chord(mode, BBOX2, 0, 180) | ||||
|         helper_chord(mode, BBOX2, 0.5, 180.4) | ||||
| 
 | ||||
| 
 | ||||
| def test_chord_width(): | ||||
|  | @ -240,6 +251,18 @@ def test_chord_zero_width(): | |||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def test_chord_too_fat(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (100, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|     # Act | ||||
|     draw.chord([-150, -150, 99, 99], 15, 60, width=10, fill="white", outline="red") | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_equal(im, Image.open("Tests/images/imagedraw_chord_too_fat.png")) | ||||
| 
 | ||||
| 
 | ||||
| def helper_ellipse(mode, bbox): | ||||
|     # Arrange | ||||
|     im = Image.new(mode, (W, H)) | ||||
|  | @ -282,15 +305,18 @@ def test_ellipse_edge(): | |||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|     # Act | ||||
|     draw.ellipse(((0, 0), (W - 1, H)), fill="white") | ||||
|     draw.ellipse(((0, 0), (W - 1, H - 1)), fill="white") | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_ellipse_symmetric(): | ||||
|     for bbox in [(25, 25, 76, 76), (25, 25, 75, 75)]: | ||||
|         im = Image.new("RGB", (101, 101)) | ||||
|     for width, bbox in ( | ||||
|         (100, (24, 24, 75, 75)), | ||||
|         (101, (25, 25, 75, 75)), | ||||
|     ): | ||||
|         im = Image.new("RGB", (width, 100)) | ||||
|         draw = ImageDraw.Draw(im) | ||||
|         draw.ellipse(bbox, fill="green", outline="blue") | ||||
|         assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) | ||||
|  | @ -345,6 +371,43 @@ def test_ellipse_zero_width(): | |||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def ellipse_various_sizes_helper(filled): | ||||
|     ellipse_sizes = range(32) | ||||
|     image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 | ||||
|     im = Image.new("RGB", (image_size, image_size)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|     x = 1 | ||||
|     for w in ellipse_sizes: | ||||
|         y = 1 | ||||
|         for h in ellipse_sizes: | ||||
|             border = [x, y, x + w - 1, y + h - 1] | ||||
|             if filled: | ||||
|                 draw.ellipse(border, fill="white") | ||||
|             else: | ||||
|                 draw.ellipse(border, outline="white") | ||||
|             y += h + 1 | ||||
|         x += w + 1 | ||||
| 
 | ||||
|     return im | ||||
| 
 | ||||
| 
 | ||||
| def test_ellipse_various_sizes(): | ||||
|     im = ellipse_various_sizes_helper(False) | ||||
| 
 | ||||
|     with Image.open("Tests/images/imagedraw_ellipse_various_sizes.png") as expected: | ||||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def test_ellipse_various_sizes_filled(): | ||||
|     im = ellipse_various_sizes_helper(True) | ||||
| 
 | ||||
|     with Image.open( | ||||
|         "Tests/images/imagedraw_ellipse_various_sizes_filled.png" | ||||
|     ) as expected: | ||||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def helper_line(points): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|  | @ -420,13 +483,13 @@ def helper_pieslice(bbox, start, end): | |||
| 
 | ||||
| 
 | ||||
| def test_pieslice1(): | ||||
|     helper_pieslice(BBOX1, -90, 45) | ||||
|     helper_pieslice(BBOX1, -90.5, 45.4) | ||||
|     helper_pieslice(BBOX1, -92, 46) | ||||
|     helper_pieslice(BBOX1, -92.2, 46.2) | ||||
| 
 | ||||
| 
 | ||||
| def test_pieslice2(): | ||||
|     helper_pieslice(BBOX2, -90, 45) | ||||
|     helper_pieslice(BBOX2, -90.5, 45.4) | ||||
|     helper_pieslice(BBOX2, -92, 46) | ||||
|     helper_pieslice(BBOX2, -92.2, 46.2) | ||||
| 
 | ||||
| 
 | ||||
| def test_pieslice_width(): | ||||
|  | @ -467,6 +530,18 @@ def test_pieslice_zero_width(): | |||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def test_pieslice_wide(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (200, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|     # Act | ||||
|     draw.pieslice([0, 0, 199, 99], 190, 170, width=10, fill="white", outline="red") | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_equal(im, Image.open("Tests/images/imagedraw_pieslice_wide.png")) | ||||
| 
 | ||||
| 
 | ||||
| def helper_point(points): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|  |  | |||
|  | @ -79,7 +79,7 @@ def test_ellipse_edge(): | |||
|     brush = ImageDraw2.Brush("white") | ||||
| 
 | ||||
|     # Act | ||||
|     draw.ellipse(((0, 0), (W - 1, H)), brush) | ||||
|     draw.ellipse(((0, 0), (W - 1, H - 1)), brush) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_similar(im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ from .helper import ( | |||
|     is_pypy, | ||||
|     is_win32, | ||||
|     skip_unless_feature, | ||||
|     skip_unless_feature_version, | ||||
| ) | ||||
| 
 | ||||
| FONT_PATH = "Tests/fonts/FreeMono.ttf" | ||||
|  | @ -901,18 +902,120 @@ class TestImageFont: | |||
|                 lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), | ||||
|             ) | ||||
| 
 | ||||
|     @skip_unless_feature("freetype2") | ||||
|     @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) | ||||
|     def test_bitmap_font(self, bpp): | ||||
|         text = "Bitmap Font" | ||||
|         layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE] | ||||
|         target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" | ||||
|         font = ImageFont.truetype( | ||||
|             f"Tests/fonts/DejaVuSans-24-{bpp}-stripped.ttf", | ||||
|             24, | ||||
|             layout_engine=self.LAYOUT_ENGINE, | ||||
|         ) | ||||
| 
 | ||||
|         im = Image.new("RGB", (160, 35), "white") | ||||
|         draw = ImageDraw.Draw(im) | ||||
|         draw.text((2, 2), text, "black", font) | ||||
| 
 | ||||
|         assert_image_equal_tofile(im, target) | ||||
| 
 | ||||
|     def test_standard_embedded_color(self): | ||||
|         txt = "Hello World!" | ||||
|         ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE) | ||||
|         ttf.getsize(txt) | ||||
| 
 | ||||
|         im = Image.new("RGB", (300, 64), "white") | ||||
|         d = ImageDraw.Draw(im) | ||||
|         d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) | ||||
| 
 | ||||
|         with Image.open("Tests/images/standard_embedded.png") as expected: | ||||
|             assert_image_similar(im, expected, max(self.metrics["multiline"], 3)) | ||||
| 
 | ||||
|     @skip_unless_feature_version("freetype2", "2.5.0") | ||||
|     @pytest.mark.xfail(is_pypy(), reason="failing on PyPy with Raqm") | ||||
|     def test_cbdt(self): | ||||
|         try: | ||||
|             font = ImageFont.truetype( | ||||
|                 "Tests/fonts/NotoColorEmoji.ttf", | ||||
|                 size=109, | ||||
|                 layout_engine=self.LAYOUT_ENGINE, | ||||
|             ) | ||||
| 
 | ||||
|             im = Image.new("RGB", (150, 150), "white") | ||||
|             d = ImageDraw.Draw(im) | ||||
| 
 | ||||
|             d.text((10, 10), "\U0001f469", embedded_color=True, font=font) | ||||
| 
 | ||||
|             with Image.open("Tests/images/cbdt_notocoloremoji.png") as expected: | ||||
|                 assert_image_similar(im, expected, self.metrics["multiline"]) | ||||
|         except IOError as e: | ||||
|             assert str(e) in ("unimplemented feature", "unknown file format") | ||||
|             pytest.skip("freetype compiled without libpng or unsupported") | ||||
| 
 | ||||
|     @skip_unless_feature_version("freetype2", "2.5.0") | ||||
|     @pytest.mark.xfail(is_pypy(), reason="failing on PyPy with Raqm") | ||||
|     def test_cbdt_mask(self): | ||||
|         try: | ||||
|             font = ImageFont.truetype( | ||||
|                 "Tests/fonts/NotoColorEmoji.ttf", | ||||
|                 size=109, | ||||
|                 layout_engine=self.LAYOUT_ENGINE, | ||||
|             ) | ||||
| 
 | ||||
|             im = Image.new("RGB", (150, 150), "white") | ||||
|             d = ImageDraw.Draw(im) | ||||
| 
 | ||||
|             d.text((10, 10), "\U0001f469", "black", font=font) | ||||
| 
 | ||||
|             with Image.open("Tests/images/cbdt_notocoloremoji_mask.png") as expected: | ||||
|                 assert_image_similar(im, expected, self.metrics["multiline"]) | ||||
|         except IOError as e: | ||||
|             assert str(e) in ("unimplemented feature", "unknown file format") | ||||
|             pytest.skip("freetype compiled without libpng or unsupported") | ||||
| 
 | ||||
|     @skip_unless_feature_version("freetype2", "2.10.0") | ||||
|     def test_colr(self): | ||||
|         font = ImageFont.truetype( | ||||
|             "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", | ||||
|             size=64, | ||||
|             layout_engine=self.LAYOUT_ENGINE, | ||||
|         ) | ||||
| 
 | ||||
|         im = Image.new("RGB", (300, 75), "white") | ||||
|         d = ImageDraw.Draw(im) | ||||
| 
 | ||||
|         d.text((15, 5), "Bungee", embedded_color=True, font=font) | ||||
| 
 | ||||
|         with Image.open("Tests/images/colr_bungee.png") as expected: | ||||
|             assert_image_similar(im, expected, 21) | ||||
| 
 | ||||
|     @skip_unless_feature_version("freetype2", "2.10.0") | ||||
|     def test_colr_mask(self): | ||||
|         font = ImageFont.truetype( | ||||
|             "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", | ||||
|             size=64, | ||||
|             layout_engine=self.LAYOUT_ENGINE, | ||||
|         ) | ||||
| 
 | ||||
|         im = Image.new("RGB", (300, 75), "white") | ||||
|         d = ImageDraw.Draw(im) | ||||
| 
 | ||||
|         d.text((15, 5), "Bungee", "black", font=font) | ||||
| 
 | ||||
|         with Image.open("Tests/images/colr_bungee_mask.png") as expected: | ||||
|             assert_image_similar(im, expected, 22) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("raqm") | ||||
| class TestImageFont_RaqmLayout(TestImageFont): | ||||
|     LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature_version("freetype2", "2.4", "Different metrics") | ||||
| def test_render_mono_size(): | ||||
|     # issue 4177 | ||||
| 
 | ||||
|     if parse_version(ImageFont.core.freetype2_version) < parse_version("2.4"): | ||||
|         pytest.skip("Different metrics") | ||||
| 
 | ||||
|     im = Image.new("P", (100, 30), "white") | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     ttf = ImageFont.truetype( | ||||
|  |  | |||
|  | @ -1,42 +0,0 @@ | |||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageDraw, ImageFont | ||||
| 
 | ||||
| from .helper import assert_image_similar | ||||
| 
 | ||||
| image_font_installed = True | ||||
| try: | ||||
|     ImageFont.core.getfont | ||||
| except ImportError: | ||||
|     image_font_installed = False | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(not image_font_installed, reason="Image font not installed") | ||||
| def test_similar(): | ||||
|     text = "EmbeddedBitmap" | ||||
|     font_outline = ImageFont.truetype(font="Tests/fonts/DejaVuSans.ttf", size=24) | ||||
|     font_bitmap = ImageFont.truetype(font="Tests/fonts/DejaVuSans-bitmap.ttf", size=24) | ||||
|     size_outline = font_outline.getsize(text) | ||||
|     size_bitmap = font_bitmap.getsize(text) | ||||
|     size_final = ( | ||||
|         max(size_outline[0], size_bitmap[0]), | ||||
|         max(size_outline[1], size_bitmap[1]), | ||||
|     ) | ||||
|     im_bitmap = Image.new("RGB", size_final, (255, 255, 255)) | ||||
|     im_outline = im_bitmap.copy() | ||||
|     draw_bitmap = ImageDraw.Draw(im_bitmap) | ||||
|     draw_outline = ImageDraw.Draw(im_outline) | ||||
| 
 | ||||
|     # Metrics are different on the bitmap and TTF fonts, | ||||
|     # more so on some platforms and versions of FreeType than others. | ||||
|     # Mac has a 1px difference, Linux doesn't. | ||||
|     draw_bitmap.text( | ||||
|         (0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap | ||||
|     ) | ||||
|     draw_outline.text( | ||||
|         (0, size_final[1] - size_outline[1]), | ||||
|         text, | ||||
|         fill=(0, 0, 0), | ||||
|         font=font_outline, | ||||
|     ) | ||||
|     assert_image_similar(im_bitmap, im_outline, 20) | ||||
|  | @ -3,7 +3,11 @@ from packaging.version import parse as parse_version | |||
| 
 | ||||
| from PIL import Image, ImageDraw, ImageFont, features | ||||
| 
 | ||||
| from .helper import assert_image_similar, skip_unless_feature | ||||
| from .helper import ( | ||||
|     assert_image_similar, | ||||
|     skip_unless_feature, | ||||
|     skip_unless_feature_version, | ||||
| ) | ||||
| 
 | ||||
| FONT_SIZE = 20 | ||||
| FONT_PATH = "Tests/fonts/DejaVuSans.ttf" | ||||
|  | @ -262,13 +266,13 @@ def test_getlength_combine(mode, direction, text): | |||
|             pytest.skip("libraqm 0.7 or greater not available") | ||||
| 
 | ||||
| 
 | ||||
| # FreeType 2.5.1 README: Miscellaneous Changes: | ||||
| # Improved computation of emulated vertical metrics for TrueType fonts. | ||||
| @skip_unless_feature_version( | ||||
|     "freetype2", "2.5.1", "FreeType <2.5.1 has incompatible ttb metrics" | ||||
| ) | ||||
| @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) | ||||
| def test_anchor_ttb(anchor): | ||||
|     if parse_version(features.version_module("freetype2")) < parse_version("2.5.1"): | ||||
|         # FreeType 2.5.1 README: Miscellaneous Changes: | ||||
|         # Improved computation of emulated vertical metrics for TrueType fonts. | ||||
|         pytest.skip("FreeType <2.5.1 has incompatible ttb metrics") | ||||
| 
 | ||||
|     text = "f" | ||||
|     path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" | ||||
|     f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) | ||||
|  |  | |||
|  | @ -438,11 +438,11 @@ These platforms are built and tested for every change. | |||
| +----------------------------------+--------------------------+-----------------------+ | ||||
| | Fedora 32                        | 3.8                      |x86-64                 | | ||||
| +----------------------------------+--------------------------+-----------------------+ | ||||
| | macOS 10.15 Catalina             | 3.6, 3.7, 3.8, PyPy3     |x86-64                 | | ||||
| | macOS 10.15 Catalina             | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64                 | | ||||
| +----------------------------------+--------------------------+-----------------------+ | ||||
| | Ubuntu Linux 16.04 LTS (Xenial)  | 3.6, 3.7, 3.8, PyPy3     |x86-64                 | | ||||
| +----------------------------------+--------------------------+-----------------------+ | ||||
| | Ubuntu Linux 18.04 LTS (Bionic)  | 3.6, 3.7, 3.8, PyPy3     |x86-64                 | | ||||
| | Ubuntu Linux 18.04 LTS (Bionic)  | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64                 | | ||||
| +----------------------------------+--------------------------+-----------------------+ | ||||
| | Ubuntu Linux 20.04 LTS (Focal)   | 3.8                      |x86-64                 | | ||||
| +----------------------------------+--------------------------+-----------------------+ | ||||
|  | @ -450,7 +450,7 @@ These platforms are built and tested for every change. | |||
| |                                  +--------------------------+-----------------------+ | ||||
| |                                  | 3.6                      |x86-64                 | | ||||
| +----------------------------------+--------------------------+-----------------------+ | ||||
| | Windows Server 2019              | 3.6, 3.7, 3.8            |x86, x86-64            | | ||||
| | Windows Server 2019              | 3.6, 3.7, 3.8, 3.9       |x86, x86-64            | | ||||
| |                                  +--------------------------+-----------------------+ | ||||
| |                                  | PyPy3                    |x86                    | | ||||
| |                                  +--------------------------+-----------------------+ | ||||
|  |  | |||
|  | @ -291,7 +291,7 @@ Methods | |||
| 
 | ||||
|     Draw a shape. | ||||
| 
 | ||||
| .. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None) | ||||
| .. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False) | ||||
| 
 | ||||
|     Draws the string at the given position. | ||||
| 
 | ||||
|  | @ -352,7 +352,12 @@ Methods | |||
| 
 | ||||
|         .. versionadded:: 6.2.0 | ||||
| 
 | ||||
| .. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None) | ||||
|     :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). | ||||
| 
 | ||||
|                     .. versionadded:: 8.0.0 | ||||
| 
 | ||||
| 
 | ||||
| .. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False) | ||||
| 
 | ||||
|     Draws the string at the given position. | ||||
| 
 | ||||
|  | @ -399,6 +404,19 @@ Methods | |||
| 
 | ||||
|                      .. versionadded:: 6.0.0 | ||||
| 
 | ||||
|     :param stroke_width: The width of the text stroke. | ||||
| 
 | ||||
|                      .. versionadded:: 6.2.0 | ||||
| 
 | ||||
|     :param stroke_fill: Color to use for the text stroke. If not given, will default to | ||||
|                         the ``fill`` parameter. | ||||
| 
 | ||||
|                      .. versionadded:: 6.2.0 | ||||
| 
 | ||||
|     :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). | ||||
| 
 | ||||
|                      .. versionadded:: 8.0.0 | ||||
| 
 | ||||
| .. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) | ||||
| 
 | ||||
|     Return the size of the given string, in pixels. | ||||
|  |  | |||
|  | @ -86,15 +86,11 @@ A new method :py:meth:`.ImageDraw.regular_polygon`, draws a regular polygon of ` | |||
| For example ``draw.regular_polygon(((100, 100), 50), 5)`` | ||||
| draws a pentagon centered at the point ``(100, 100)`` with a polygon radius of ``50``. | ||||
| 
 | ||||
| Security | ||||
| ======== | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| Other Changes | ||||
| ============= | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| Error for large BMP files | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| TODO | ||||
| Previously, if a BMP file was too large, an ``OSError`` would be raised. Now, | ||||
| ``DecompressionBombError`` is used instead, as Pillow already uses for other formats. | ||||
|  |  | |||
|  | @ -162,10 +162,6 @@ class BmpImageFile(ImageFile.ImageFile): | |||
|             else (1 << file_info["bits"]) | ||||
|         ) | ||||
| 
 | ||||
|         # ------------------------------- Check abnormal values for DOS attacks | ||||
|         if file_info["width"] * file_info["height"] > 2 ** 31: | ||||
|             raise OSError("Unsupported BMP Size: (%dx%d)" % self.size) | ||||
| 
 | ||||
|         # ---------------------- Check bit depth for unusual unsupported values | ||||
|         self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) | ||||
|         if self.mode is None: | ||||
|  |  | |||
|  | @ -3285,7 +3285,7 @@ class Exif(MutableMapping): | |||
|         return value | ||||
| 
 | ||||
|     def _fixup_dict(self, src_dict): | ||||
|         # Helper function for _getexif() | ||||
|         # Helper function | ||||
|         # returns a dict with any single item tuples/lists as individual values | ||||
|         return {k: self._fixup(v) for k, v in src_dict.items()} | ||||
| 
 | ||||
|  |  | |||
|  | @ -118,7 +118,7 @@ class ImageDraw: | |||
|                 fill = self.draw.draw_ink(fill) | ||||
|         return ink, fill | ||||
| 
 | ||||
|     def arc(self, xy, start, end, fill=None, width=0): | ||||
|     def arc(self, xy, start, end, fill=None, width=1): | ||||
|         """Draw an arc.""" | ||||
|         ink, fill = self._getink(fill) | ||||
|         if ink is not None: | ||||
|  | @ -282,6 +282,7 @@ class ImageDraw: | |||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         stroke_fill=None, | ||||
|         embedded_color=False, | ||||
|         *args, | ||||
|         **kwargs, | ||||
|     ): | ||||
|  | @ -299,8 +300,12 @@ class ImageDraw: | |||
|                 language, | ||||
|                 stroke_width, | ||||
|                 stroke_fill, | ||||
|                 embedded_color, | ||||
|             ) | ||||
| 
 | ||||
|         if embedded_color and self.mode not in ("RGB", "RGBA"): | ||||
|             raise ValueError("Embedded color supported only in RGB and RGBA modes") | ||||
| 
 | ||||
|         if font is None: | ||||
|             font = self.getfont() | ||||
| 
 | ||||
|  | @ -311,16 +316,20 @@ class ImageDraw: | |||
|             return ink | ||||
| 
 | ||||
|         def draw_text(ink, stroke_width=0, stroke_offset=None): | ||||
|             mode = self.fontmode | ||||
|             if stroke_width == 0 and embedded_color: | ||||
|                 mode = "RGBA" | ||||
|             coord = xy | ||||
|             try: | ||||
|                 mask, offset = font.getmask2( | ||||
|                     text, | ||||
|                     self.fontmode, | ||||
|                     mode, | ||||
|                     direction=direction, | ||||
|                     features=features, | ||||
|                     language=language, | ||||
|                     stroke_width=stroke_width, | ||||
|                     anchor=anchor, | ||||
|                     ink=ink, | ||||
|                     *args, | ||||
|                     **kwargs, | ||||
|                 ) | ||||
|  | @ -329,12 +338,13 @@ class ImageDraw: | |||
|                 try: | ||||
|                     mask = font.getmask( | ||||
|                         text, | ||||
|                         self.fontmode, | ||||
|                         mode, | ||||
|                         direction, | ||||
|                         features, | ||||
|                         language, | ||||
|                         stroke_width, | ||||
|                         anchor, | ||||
|                         ink, | ||||
|                         *args, | ||||
|                         **kwargs, | ||||
|                     ) | ||||
|  | @ -342,7 +352,15 @@ class ImageDraw: | |||
|                     mask = font.getmask(text) | ||||
|             if stroke_offset: | ||||
|                 coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] | ||||
|             self.draw.draw_bitmap(coord, mask, ink) | ||||
|             if mode == "RGBA": | ||||
|                 # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A | ||||
|                 # extract mask and set text alpha | ||||
|                 color, mask = mask, mask.getband(3) | ||||
|                 color.fillband(3, (ink >> 24) & 0xFF) | ||||
|                 coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1] | ||||
|                 self.im.paste(color, coord + coord2, mask) | ||||
|             else: | ||||
|                 self.draw.draw_bitmap(coord, mask, ink) | ||||
| 
 | ||||
|         ink = getink(fill) | ||||
|         if ink is not None: | ||||
|  | @ -374,6 +392,7 @@ class ImageDraw: | |||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         stroke_fill=None, | ||||
|         embedded_color=False, | ||||
|     ): | ||||
|         if direction == "ttb": | ||||
|             raise ValueError("ttb direction is unsupported for multiline text") | ||||
|  | @ -435,6 +454,7 @@ class ImageDraw: | |||
|                 language=language, | ||||
|                 stroke_width=stroke_width, | ||||
|                 stroke_fill=stroke_fill, | ||||
|                 embedded_color=embedded_color, | ||||
|             ) | ||||
|             top += line_spacing | ||||
| 
 | ||||
|  |  | |||
|  | @ -413,7 +413,7 @@ class FreeTypeFont: | |||
|         """ | ||||
|         # vertical offset is added for historical reasons | ||||
|         # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 | ||||
|         size, offset = self.font.getsize(text, False, direction, features, language) | ||||
|         size, offset = self.font.getsize(text, "L", direction, features, language) | ||||
|         return ( | ||||
|             size[0] + stroke_width * 2, | ||||
|             size[1] + stroke_width * 2 + offset[1], | ||||
|  | @ -500,12 +500,14 @@ class FreeTypeFont: | |||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         anchor=None, | ||||
|         ink=0, | ||||
|     ): | ||||
|         """ | ||||
|         Create a bitmap for the text. | ||||
| 
 | ||||
|         If the font uses antialiasing, the bitmap should have mode ``L`` and use a | ||||
|         maximum value of 255. Otherwise, it should have mode ``1``. | ||||
|         maximum value of 255. If the font has embedded color data, the bitmap | ||||
|         should have mode ``RGBA``. Otherwise, it should have mode ``1``. | ||||
| 
 | ||||
|         :param text: Text to render. | ||||
|         :param mode: Used by some graphics drivers to indicate what mode the | ||||
|  | @ -554,6 +556,10 @@ class FreeTypeFont: | |||
| 
 | ||||
|                          .. versionadded:: 8.0.0 | ||||
| 
 | ||||
|         :param ink: Foreground ink for rendering in RGBA mode. | ||||
| 
 | ||||
|                          .. versionadded:: 8.0.0 | ||||
| 
 | ||||
|         :return: An internal PIL storage memory instance as defined by the | ||||
|                  :py:mod:`PIL.Image.core` interface module. | ||||
|         """ | ||||
|  | @ -565,6 +571,7 @@ class FreeTypeFont: | |||
|             language=language, | ||||
|             stroke_width=stroke_width, | ||||
|             anchor=anchor, | ||||
|             ink=ink, | ||||
|         )[0] | ||||
| 
 | ||||
|     def getmask2( | ||||
|  | @ -577,6 +584,7 @@ class FreeTypeFont: | |||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         anchor=None, | ||||
|         ink=0, | ||||
|         *args, | ||||
|         **kwargs, | ||||
|     ): | ||||
|  | @ -584,7 +592,8 @@ class FreeTypeFont: | |||
|         Create a bitmap for the text. | ||||
| 
 | ||||
|         If the font uses antialiasing, the bitmap should have mode ``L`` and use a | ||||
|         maximum value of 255. Otherwise, it should have mode ``1``. | ||||
|         maximum value of 255. If the font has embedded color data, the bitmap | ||||
|         should have mode ``RGBA``. Otherwise, it should have mode ``1``. | ||||
| 
 | ||||
|         :param text: Text to render. | ||||
|         :param mode: Used by some graphics drivers to indicate what mode the | ||||
|  | @ -633,18 +642,22 @@ class FreeTypeFont: | |||
| 
 | ||||
|                          .. versionadded:: 8.0.0 | ||||
| 
 | ||||
|         :param ink: Foreground ink for rendering in RGBA mode. | ||||
| 
 | ||||
|                          .. versionadded:: 8.0.0 | ||||
| 
 | ||||
|         :return: A tuple of an internal PIL storage memory instance as defined by the | ||||
|                  :py:mod:`PIL.Image.core` interface module, and the text offset, the | ||||
|                  gap between the starting coordinate and the first marking | ||||
|         """ | ||||
|         size, offset = self.font.getsize( | ||||
|             text, mode == "1", direction, features, language, anchor | ||||
|             text, mode, direction, features, language, anchor | ||||
|         ) | ||||
|         size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 | ||||
|         offset = offset[0] - stroke_width, offset[1] - stroke_width | ||||
|         im = fill("L", size, 0) | ||||
|         im = fill("RGBA" if mode == "RGBA" else "L", size, 0) | ||||
|         self.font.render( | ||||
|             text, im.id, mode == "1", direction, features, language, stroke_width | ||||
|             text, im.id, mode, direction, features, language, stroke_width, ink | ||||
|         ) | ||||
|         return im, offset | ||||
| 
 | ||||
|  |  | |||
|  | @ -475,13 +475,6 @@ class JpegImageFile(ImageFile.ImageFile): | |||
|         return _getmp(self) | ||||
| 
 | ||||
| 
 | ||||
| def _fixup_dict(src_dict): | ||||
|     # Helper function for _getexif() | ||||
|     # returns a dict with any single item tuples/lists as individual values | ||||
|     exif = Image.Exif() | ||||
|     return exif._fixup_dict(src_dict) | ||||
| 
 | ||||
| 
 | ||||
| def _getexif(self): | ||||
|     if "exif" not in self.info: | ||||
|         return None | ||||
|  |  | |||
|  | @ -518,6 +518,9 @@ getink(PyObject* color, Imaging im, char* ink) | |||
|        be cast to either UINT8 or INT32 */ | ||||
| 
 | ||||
|     int rIsInt = 0; | ||||
|     if (PyTuple_Check(color) && PyTuple_Size(color) == 1) { | ||||
|         color = PyTuple_GetItem(color, 0); | ||||
|     } | ||||
|     if (im->type == IMAGING_TYPE_UINT8 || | ||||
|         im->type == IMAGING_TYPE_INT32 || | ||||
|         im->type == IMAGING_TYPE_SPECIAL) { | ||||
|  | @ -533,7 +536,7 @@ getink(PyObject* color, Imaging im, char* ink) | |||
|                 return NULL; | ||||
|             } | ||||
|         } else { | ||||
|             PyErr_SetString(PyExc_TypeError, "color must be int"); | ||||
|             PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); | ||||
|             return NULL; | ||||
|         } | ||||
|     } | ||||
|  | @ -2836,8 +2839,7 @@ _draw_arc(ImagingDrawObject* self, PyObject* args) | |||
|     int ink; | ||||
|     int width = 0; | ||||
|     float start, end; | ||||
|     int op = 0; | ||||
|     if (!PyArg_ParseTuple(args, "Offi|ii", &data, &start, &end, &ink, &width)) { | ||||
|     if (!PyArg_ParseTuple(args, "Offi|i", &data, &start, &end, &ink, &width)) { | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|  | @ -2854,7 +2856,7 @@ _draw_arc(ImagingDrawObject* self, PyObject* args) | |||
|     n = ImagingDrawArc(self->image->image, | ||||
|                        (int) xy[0], (int) xy[1], | ||||
|                        (int) xy[2], (int) xy[3], | ||||
|                        start, end, &ink, width, op | ||||
|                        start, end, &ink, width, self->blend | ||||
|                        ); | ||||
| 
 | ||||
|     free(xy); | ||||
|  |  | |||
							
								
								
									
										204
									
								
								src/_imagingft.c
									
									
									
									
									
								
							
							
						
						|  | @ -25,9 +25,13 @@ | |||
| #include <ft2build.h> | ||||
| #include FT_FREETYPE_H | ||||
| #include FT_GLYPH_H | ||||
| #include FT_BITMAP_H | ||||
| #include FT_STROKER_H | ||||
| #include FT_MULTIPLE_MASTERS_H | ||||
| #include FT_SFNT_NAMES_H | ||||
| #ifdef FT_COLOR_H | ||||
| #include FT_COLOR_H | ||||
| #endif | ||||
| 
 | ||||
| #define KEEP_PY_UNICODE | ||||
| 
 | ||||
|  | @ -350,7 +354,7 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) | |||
| 
 | ||||
| static size_t | ||||
| text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *features, | ||||
|                  const char* lang, GlyphInfo **glyph_info, int mask) | ||||
|                  const char* lang, GlyphInfo **glyph_info, int mask, int color) | ||||
| { | ||||
|     size_t i = 0, count = 0, start = 0; | ||||
|     raqm_t *rq; | ||||
|  | @ -529,7 +533,7 @@ failed: | |||
| 
 | ||||
| static size_t | ||||
| text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObject *features, | ||||
|                      const char* lang, GlyphInfo **glyph_info, int mask) | ||||
|                      const char* lang, GlyphInfo **glyph_info, int mask, int color) | ||||
| { | ||||
|     int error, load_flags; | ||||
|     FT_ULong ch; | ||||
|  | @ -561,10 +565,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje | |||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     load_flags = FT_LOAD_NO_BITMAP; | ||||
|     load_flags = FT_LOAD_DEFAULT; | ||||
|     if (mask) { | ||||
|         load_flags |= FT_LOAD_TARGET_MONO; | ||||
|     } | ||||
| #ifdef FT_LOAD_COLOR | ||||
|     if (color) { | ||||
|         load_flags |= FT_LOAD_COLOR; | ||||
|     } | ||||
| #endif | ||||
|     for (i = 0; font_getchar(string, i, &ch); i++) { | ||||
|         (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); | ||||
|         error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); | ||||
|  | @ -595,14 +604,14 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObje | |||
| 
 | ||||
| static size_t | ||||
| text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *features, | ||||
|             const char* lang, GlyphInfo **glyph_info, int mask) | ||||
|             const char* lang, GlyphInfo **glyph_info, int mask, int color) | ||||
| { | ||||
|     size_t count; | ||||
| 
 | ||||
|     if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) { | ||||
|         count = text_layout_raqm(string, self, dir, features, lang, glyph_info,  mask); | ||||
|         count = text_layout_raqm(string, self, dir, features, lang, glyph_info,  mask, color); | ||||
|     } else { | ||||
|         count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask); | ||||
|         count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask, color); | ||||
|     } | ||||
|     return count; | ||||
| } | ||||
|  | @ -667,6 +676,8 @@ font_getsize(FontObject* self, PyObject* args) | |||
|     size_t i, count; /* glyph_info index and length */ | ||||
|     int horizontal_dir; /* is primary axis horizontal? */ | ||||
|     int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ | ||||
|     int color = 0; /* is FT_LOAD_COLOR enabled? */ | ||||
|     const char *mode = NULL; | ||||
|     const char *dir = NULL; | ||||
|     const char *lang = NULL; | ||||
|     const char *anchor = NULL; | ||||
|  | @ -675,12 +686,15 @@ font_getsize(FontObject* self, PyObject* args) | |||
| 
 | ||||
|     /* calculate size and bearing for a given string */ | ||||
| 
 | ||||
|     if (!PyArg_ParseTuple(args, "O|izOzz:getsize", &string, &mask, &dir, &features, &lang, &anchor)) { | ||||
|     if (!PyArg_ParseTuple(args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; | ||||
| 
 | ||||
|     mask = mode && strcmp(mode, "1") == 0; | ||||
|     color = mode && strcmp(mode, "RGBA") == 0; | ||||
| 
 | ||||
|     if (anchor == NULL) { | ||||
|         anchor = horizontal_dir ? "la" : "lt"; | ||||
|     } | ||||
|  | @ -688,18 +702,20 @@ font_getsize(FontObject* self, PyObject* args) | |||
|         goto bad_anchor; | ||||
|     } | ||||
| 
 | ||||
|     count = text_layout(string, self, dir, features, lang, &glyph_info, mask); | ||||
|     count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); | ||||
|     if (PyErr_Occurred()) { | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960
 | ||||
|      *   Yifu Yu<root@jackyyf.com>, 2014-10-15 | ||||
|      */ | ||||
|     load_flags = FT_LOAD_NO_BITMAP; | ||||
|     load_flags = FT_LOAD_DEFAULT; | ||||
|     if (mask) { | ||||
|         load_flags |= FT_LOAD_TARGET_MONO; | ||||
|     } | ||||
| #ifdef FT_LOAD_COLOR | ||||
|     if (color) { | ||||
|         load_flags |= FT_LOAD_COLOR; | ||||
|     } | ||||
| #endif | ||||
| 
 | ||||
|     /*
 | ||||
|      * text bounds are given by: | ||||
|  | @ -865,19 +881,26 @@ font_render(FontObject* self, PyObject* args) | |||
|     FT_Glyph glyph; | ||||
|     FT_GlyphSlot glyph_slot; | ||||
|     FT_Bitmap bitmap; | ||||
|     FT_Bitmap bitmap_converted; /* initialized lazily, for non-8bpp fonts */ | ||||
|     FT_BitmapGlyph bitmap_glyph; | ||||
|     FT_Stroker stroker = NULL; | ||||
|     int bitmap_converted_ready = 0; /* has bitmap_converted been initialized */ | ||||
|     GlyphInfo *glyph_info = NULL; /* computed text layout */ | ||||
|     size_t i, count; /* glyph_info index and length */ | ||||
|     int xx, yy; /* pixel offset of current glyph bitmap */ | ||||
|     int x0, x1; /* horizontal bounds of glyph bitmap to copy */ | ||||
|     unsigned int bitmap_y; /* glyph bitmap y index */ | ||||
|     unsigned char *source; /* glyph bitmap source buffer */ | ||||
|     unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ | ||||
|     Imaging im; | ||||
|     Py_ssize_t id; | ||||
|     int horizontal_dir; /* is primary axis horizontal? */ | ||||
|     int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ | ||||
|     int color = 0; /* is FT_LOAD_COLOR enabled? */ | ||||
|     int stroke_width = 0; | ||||
|     PY_LONG_LONG foreground_ink_long = 0; | ||||
|     unsigned int foreground_ink; | ||||
|     const char *mode = NULL; | ||||
|     const char *dir = NULL; | ||||
|     const char *lang = NULL; | ||||
|     PyObject *features = Py_None; | ||||
|  | @ -886,14 +909,31 @@ font_render(FontObject* self, PyObject* args) | |||
|     /* render string into given buffer (the buffer *must* have
 | ||||
|        the right size, or this will crash) */ | ||||
| 
 | ||||
|     if (!PyArg_ParseTuple(args, "On|izOzi:render", &string,  &id, &mask, &dir, &features, &lang, | ||||
|                                                    &stroke_width)) { | ||||
|     if (!PyArg_ParseTuple(args, "On|zzOziL:render", &string,  &id, &mode, &dir, &features, &lang, | ||||
|                                                    &stroke_width, &foreground_ink_long)) { | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; | ||||
| 
 | ||||
|     count = text_layout(string, self, dir, features, lang, &glyph_info, mask); | ||||
|     mask = mode && strcmp(mode, "1") == 0; | ||||
|     color = mode && strcmp(mode, "RGBA") == 0; | ||||
| 
 | ||||
|     foreground_ink = foreground_ink_long; | ||||
| 
 | ||||
| #ifdef FT_COLOR_H | ||||
|     if (color) { | ||||
|         FT_Color foreground_color; | ||||
|         FT_Byte* ink = (FT_Byte*)&foreground_ink; | ||||
|         foreground_color.red = ink[0]; | ||||
|         foreground_color.green = ink[1]; | ||||
|         foreground_color.blue = ink[2]; | ||||
|         foreground_color.alpha = (FT_Byte) 255; /* ink alpha is handled in ImageDraw.text */ | ||||
|         FT_Palette_Set_Foreground_Color(self->face, foreground_color); | ||||
|     } | ||||
| #endif | ||||
| 
 | ||||
|     count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); | ||||
|     if (PyErr_Occurred()) { | ||||
|         return NULL; | ||||
|     } | ||||
|  | @ -911,12 +951,15 @@ font_render(FontObject* self, PyObject* args) | |||
|     } | ||||
| 
 | ||||
|     im = (Imaging) id; | ||||
| 
 | ||||
|     /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ | ||||
|     load_flags = FT_LOAD_NO_BITMAP; | ||||
|     load_flags = FT_LOAD_DEFAULT; | ||||
|     if (mask) { | ||||
|         load_flags |= FT_LOAD_TARGET_MONO; | ||||
|     } | ||||
| #ifdef FT_LOAD_COLOR | ||||
|     if (color) { | ||||
|         load_flags |= FT_LOAD_COLOR; | ||||
|     } | ||||
| #endif | ||||
| 
 | ||||
|     /*
 | ||||
|      * calculate x_min and y_max | ||||
|  | @ -988,6 +1031,55 @@ font_render(FontObject* self, PyObject* args) | |||
|             yy = -(py + glyph_slot->bitmap_top); | ||||
|         } | ||||
| 
 | ||||
|         /* convert non-8bpp bitmaps */ | ||||
|         switch (bitmap.pixel_mode) { | ||||
|             case FT_PIXEL_MODE_MONO: | ||||
|                 convert_scale = 255; | ||||
|                 break; | ||||
|             case FT_PIXEL_MODE_GRAY2: | ||||
|                 convert_scale = 255 / 3; | ||||
|                 break; | ||||
|             case FT_PIXEL_MODE_GRAY4: | ||||
|                 convert_scale = 255 / 15; | ||||
|                 break; | ||||
|             default: | ||||
|                 convert_scale = 1; | ||||
|         } | ||||
|         switch (bitmap.pixel_mode) { | ||||
|             case FT_PIXEL_MODE_MONO: | ||||
|             case FT_PIXEL_MODE_GRAY2: | ||||
|             case FT_PIXEL_MODE_GRAY4: | ||||
|                 if (!bitmap_converted_ready) { | ||||
| 
 | ||||
| #if FREETYPE_MAJOR > 2 ||\ | ||||
|     (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 6) | ||||
|                     FT_Bitmap_Init(&bitmap_converted); | ||||
| #else | ||||
|                     FT_Bitmap_New(&bitmap_converted); | ||||
| #endif | ||||
|                     bitmap_converted_ready = 1; | ||||
|                 } | ||||
|                 error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1); | ||||
|                 if (error) { | ||||
|                     geterror(error); | ||||
|                     goto glyph_error; | ||||
|                 } | ||||
|                 bitmap = bitmap_converted; | ||||
|                 /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ | ||||
|             case FT_PIXEL_MODE_GRAY: | ||||
|                 break; | ||||
| #ifdef FT_LOAD_COLOR | ||||
|             case FT_PIXEL_MODE_BGRA: | ||||
|                 if (color) { | ||||
|                     break; | ||||
|                 } | ||||
|                 /* we didn't ask for color, fall through to default */ | ||||
| #endif | ||||
|             default: | ||||
|                 PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); | ||||
|                 goto glyph_error; | ||||
|         } | ||||
| 
 | ||||
|         /* clip glyph bitmap width to target image bounds */ | ||||
|         x0 = 0; | ||||
|         x1 = bitmap.width; | ||||
|  | @ -1002,28 +1094,54 @@ font_render(FontObject* self, PyObject* args) | |||
|         for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++, yy++) { | ||||
|             /* clip glyph bitmap height to target image bounds */ | ||||
|             if (yy >= 0 && yy < im->ysize) { | ||||
|                 // blend this glyph into the buffer
 | ||||
|                 unsigned char *target = im->image8[yy] + xx; | ||||
|                 if (mask) { | ||||
|                     // use monochrome mask (on palette images, etc)
 | ||||
|                     int j, k, m = 128; | ||||
|                     for (j = k = 0; j < x1; j++) { | ||||
|                         if (j >= x0 && (source[k] & m)) { | ||||
|                             target[j] = 255; | ||||
|                 /* blend this glyph into the buffer */ | ||||
|                 int k; | ||||
|                 unsigned char v; | ||||
|                 unsigned char* target; | ||||
|                 if (color) { | ||||
|                     /* target[RGB] returns the color, target[A] returns the mask */ | ||||
|                     /* target bands get split again in ImageDraw.text */ | ||||
|                     target = im->image[yy] + xx * 4; | ||||
|                 } else { | ||||
|                     target = im->image8[yy] + xx; | ||||
|                 } | ||||
| #ifdef FT_LOAD_COLOR | ||||
|                 if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { | ||||
|                     /* paste color glyph */ | ||||
|                     for (k = x0; k < x1; k++) { | ||||
|                         if (target[k * 4 + 3] < source[k * 4 + 3]) { | ||||
|                             /* unpremultiply BGRa to RGBA */ | ||||
|                             target[k * 4 + 0] = CLIP8((255 * (int)source[k * 4 + 2]) / source[k * 4 + 3]); | ||||
|                             target[k * 4 + 1] = CLIP8((255 * (int)source[k * 4 + 1]) / source[k * 4 + 3]); | ||||
|                             target[k * 4 + 2] = CLIP8((255 * (int)source[k * 4 + 0]) / source[k * 4 + 3]); | ||||
|                             target[k * 4 + 3] = source[k * 4 + 3]; | ||||
|                         } | ||||
|                         if (!(m >>= 1)) { | ||||
|                             m = 128; | ||||
|                             k++; | ||||
|                     } | ||||
|                 } else | ||||
| #endif | ||||
|                 if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { | ||||
|                     if (color) { | ||||
|                         unsigned char* ink = (unsigned char*)&foreground_ink; | ||||
|                         for (k = x0; k < x1; k++) { | ||||
|                             v = source[k] * convert_scale; | ||||
|                             if (target[k * 4 + 3] < v) { | ||||
|                                 target[k * 4 + 0] = ink[0]; | ||||
|                                 target[k * 4 + 1] = ink[1]; | ||||
|                                 target[k * 4 + 2] = ink[2]; | ||||
|                                 target[k * 4 + 3] = v; | ||||
|                             } | ||||
|                         } | ||||
|                     } else { | ||||
|                         for (k = x0; k < x1; k++) { | ||||
|                             v = source[k] * convert_scale; | ||||
|                             if (target[k] < v) { | ||||
|                                 target[k] = v; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     // use antialiased rendering
 | ||||
|                     int k; | ||||
|                     for (k = x0; k < x1; k++) { | ||||
|                         if (target[k] < source[k]) { | ||||
|                             target[k] = source[k]; | ||||
|                         } | ||||
|                     } | ||||
|                     PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); | ||||
|                     goto glyph_error; | ||||
|                 } | ||||
|             } | ||||
|             source += bitmap.pitch; | ||||
|  | @ -1035,9 +1153,23 @@ font_render(FontObject* self, PyObject* args) | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (bitmap_converted_ready) { | ||||
|         FT_Bitmap_Done(library, &bitmap_converted); | ||||
|     } | ||||
|     FT_Stroker_Done(stroker); | ||||
|     PyMem_Del(glyph_info); | ||||
|     Py_RETURN_NONE; | ||||
| 
 | ||||
| glyph_error: | ||||
|     if (stroker != NULL) { | ||||
|         FT_Done_Glyph(glyph); | ||||
|     } | ||||
|     if (bitmap_converted_ready) { | ||||
|         FT_Bitmap_Done(library, &bitmap_converted); | ||||
|     } | ||||
|     FT_Stroker_Done(stroker); | ||||
|     PyMem_Del(glyph_info); | ||||
|     return NULL; | ||||
| } | ||||
| 
 | ||||
| #if FREETYPE_MAJOR > 2 ||\ | ||||
|  |  | |||
|  | @ -35,6 +35,7 @@ | |||
| #include "Imaging.h" | ||||
| 
 | ||||
| #include <math.h> | ||||
| #include <stdint.h> | ||||
| 
 | ||||
| #define CEIL(v)  (int) ceil(v) | ||||
| #define FLOOR(v) ((v) >= 0.0 ? (int) (v) : (int) floor(v)) | ||||
|  | @ -818,222 +819,751 @@ ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void* ink, | |||
| /* -------------------------------------------------------------------- */ | ||||
| /* standard shapes */ | ||||
| 
 | ||||
| #define ARC 0 | ||||
| #define CHORD 1 | ||||
| #define PIESLICE 2 | ||||
| // Imagine 2D plane and ellipse with center in (0, 0) and semi-major axes a and b.
 | ||||
| // Then quarter_* stuff approximates its top right quarter (x, y >= 0) with integer
 | ||||
| // points from set {(2x+x0, 2y+y0) | x,y in Z} where x0, y0 are from {0, 1} and
 | ||||
| // are such that point (a, b) is in the set.
 | ||||
| 
 | ||||
| static void | ||||
| ellipsePoint(int cx, int cy, int w, int h, | ||||
|              float i, int *x, int *y) | ||||
| { | ||||
|     float i_cos, i_sin; | ||||
|     float x_f, y_f; | ||||
|     double modf_int; | ||||
|     i_cos = cos(i*M_PI/180); | ||||
|     i_sin = sin(i*M_PI/180); | ||||
|     x_f = (i_cos * w/2) + cx; | ||||
|     y_f = (i_sin * h/2) + cy; | ||||
|     if (modf(x_f, &modf_int) == 0.5) { | ||||
|         *x = i_cos > 0 ? FLOOR(x_f) : CEIL(x_f); | ||||
| typedef struct { | ||||
|     int32_t a, b, cx, cy, ex, ey; | ||||
|     int64_t a2, b2, a2b2; | ||||
|     int8_t finished; | ||||
| } quarter_state; | ||||
| 
 | ||||
| void quarter_init(quarter_state* s, int32_t a, int32_t b) { | ||||
|     if (a < 0 || b < 0) { | ||||
|         s->finished = 1; | ||||
|     } else { | ||||
|         *x = FLOOR(x_f + 0.5); | ||||
|     } | ||||
|     if (modf(y_f, &modf_int) == 0.5) { | ||||
|         *y = i_sin > 0 ? FLOOR(y_f) : CEIL(y_f); | ||||
|     } else { | ||||
|         *y = FLOOR(y_f + 0.5); | ||||
|         s->a = a; | ||||
|         s->b = b; | ||||
|         s->cx = a; | ||||
|         s->cy = b % 2; | ||||
|         s->ex = a % 2; | ||||
|         s->ey = b; | ||||
|         s->a2 = a * a; | ||||
|         s->b2 = b * b; | ||||
|         s->a2b2 = s->a2 * s->b2; | ||||
|         s->finished = 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| ellipse(Imaging im, int x0, int y0, int x1, int y1, | ||||
|         float start, float end, const void* ink_, int fill, | ||||
|         int width, int mode, int op) | ||||
| { | ||||
|     float i; | ||||
|     int inner; | ||||
|     int n; | ||||
|     int maxEdgeCount; | ||||
|     int w, h; | ||||
|     int x, y; | ||||
|     int cx, cy; | ||||
|     int lx = 0, ly = 0; | ||||
|     int sx = 0, sy = 0; | ||||
|     int lx_inner = 0, ly_inner = 0; | ||||
|     int sx_inner = 0, sy_inner = 0; | ||||
|     DRAW* draw; | ||||
|     INT32 ink; | ||||
|     Edge* e; | ||||
| // deviation of the point from ellipse curve, basically a substitution
 | ||||
| // of the point into the ellipse equation
 | ||||
| int64_t quarter_delta(quarter_state* s, int64_t x, int64_t y) { | ||||
|     return llabs(s->a2 * y * y + s->b2 * x * x - s->a2b2); | ||||
| } | ||||
| 
 | ||||
|     DRAWINIT(); | ||||
| 
 | ||||
|     while (end < start) { | ||||
|         end += 360; | ||||
| int8_t quarter_next(quarter_state* s, int32_t* ret_x, int32_t* ret_y) { | ||||
|     if (s->finished) { | ||||
|         return -1; | ||||
|     } | ||||
| 
 | ||||
|     if (end - start > 360) { | ||||
|         // no need to go in loops
 | ||||
|         end = start + 361; | ||||
|     } | ||||
| 
 | ||||
|     w = x1 - x0; | ||||
|     h = y1 - y0; | ||||
|     if (w <= 0 || h <= 0) { | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     cx = (x0 + x1) / 2; | ||||
|     cy = (y0 + y1) / 2; | ||||
| 
 | ||||
|     if (!fill && width <= 1) { | ||||
|         for (i = start; i < end+1; i++) { | ||||
|             if (i > end) { | ||||
|                 i = end; | ||||
|             } | ||||
|             ellipsePoint(cx, cy, w, h, i, &x, &y); | ||||
|             if (i != start) { | ||||
|                 draw->line(im, lx, ly, x, y, ink); | ||||
|             } else { | ||||
|                 sx = x, sy = y; | ||||
|             } | ||||
|             lx = x, ly = y; | ||||
|         } | ||||
| 
 | ||||
|         if (i != start) { | ||||
|             if (mode == PIESLICE) { | ||||
|                 if (x != cx || y != cy) { | ||||
|                     draw->line(im, x, y, cx, cy, ink); | ||||
|                     draw->line(im, cx, cy, sx, sy, ink); | ||||
|                 } | ||||
|             } else if (mode == CHORD) { | ||||
|                 if (x != sx || y != sy) { | ||||
|                     draw->line(im, x, y, sx, sy, ink); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     *ret_x = s->cx; | ||||
|     *ret_y = s->cy; | ||||
|     if (s->cx == s->ex && s->cy == s->ey) { | ||||
|         s->finished = 1; | ||||
|     } else { | ||||
|         inner = (mode == ARC || !fill) ? 1 : 0; | ||||
| 
 | ||||
|         // Build edge list
 | ||||
|         // malloc check UNDONE, FLOAT?
 | ||||
|         maxEdgeCount = ceil(end - start); | ||||
|         if (inner) { | ||||
|             maxEdgeCount *= 2; | ||||
|         } | ||||
|         maxEdgeCount += 3; | ||||
|         e = calloc(maxEdgeCount, sizeof(Edge)); | ||||
|         if (!e) { | ||||
|             ImagingError_MemoryError(); | ||||
|             return -1; | ||||
|         } | ||||
| 
 | ||||
|         // Outer circle
 | ||||
|         n = 0; | ||||
|         for (i = start; i < end+1; i++) { | ||||
|             if (i > end) { | ||||
|                 i = end; | ||||
|         // Bresenham's algorithm, possible optimization: only consider 2 of 3
 | ||||
|         // next points depending on current slope
 | ||||
|         int32_t nx = s->cx; | ||||
|         int32_t ny = s->cy + 2; | ||||
|         int64_t ndelta = quarter_delta(s, nx, ny); | ||||
|         if (nx > 1) { | ||||
|             int64_t newdelta = quarter_delta(s, s->cx - 2, s->cy + 2); | ||||
|             if (ndelta > newdelta) { | ||||
|                 nx = s->cx - 2; | ||||
|                 ny = s->cy + 2; | ||||
|                 ndelta = newdelta; | ||||
|             } | ||||
|             ellipsePoint(cx, cy, w, h, i, &x, &y); | ||||
|             if (i == start) { | ||||
|                 sx = x, sy = y; | ||||
|             } else { | ||||
|                 add_edge(&e[n++], lx, ly, x, y); | ||||
|             } | ||||
|             lx = x, ly = y; | ||||
|         } | ||||
|         if (n == 0) { | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         if (inner) { | ||||
|             // Inner circle
 | ||||
|             x0 += width - 1; | ||||
|             y0 += width - 1; | ||||
|             x1 -= width - 1; | ||||
|             y1 -= width - 1; | ||||
| 
 | ||||
|             w = x1 - x0; | ||||
|             h = y1 - y0; | ||||
|             if (w <= 0 || h <= 0) { | ||||
|                 // ARC with no gap in the middle is a PIESLICE
 | ||||
|                 mode = PIESLICE; | ||||
|                 inner = 0; | ||||
|             } else { | ||||
|                 for (i = start; i < end+1; i++) { | ||||
|                     if (i > end) { | ||||
|                         i = end; | ||||
|                     } | ||||
|                     ellipsePoint(cx, cy, w, h, i, &x, &y); | ||||
|                     if (i == start) { | ||||
|                         sx_inner = x, sy_inner = y; | ||||
|                     } else { | ||||
|                         add_edge(&e[n++], lx_inner, ly_inner, x, y); | ||||
|                     } | ||||
|                     lx_inner = x, ly_inner = y; | ||||
|                 } | ||||
|             newdelta = quarter_delta(s, s->cx - 2, s->cy); | ||||
|             if (ndelta > newdelta) { | ||||
|                 nx = s->cx - 2; | ||||
|                 ny = s->cy; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (end - start < 360) { | ||||
|             // Close polygon
 | ||||
|             if (mode == PIESLICE) { | ||||
|                 if (x != cx || y != cy) { | ||||
|                     add_edge(&e[n++], sx, sy, cx, cy); | ||||
|                     add_edge(&e[n++], cx, cy, lx, ly); | ||||
|                     if (inner) { | ||||
|                         ImagingDrawWideLine(im, sx, sy, cx, cy, &ink, width, op); | ||||
|                         ImagingDrawWideLine(im, cx, cy, lx, ly, &ink, width, op); | ||||
|                     } | ||||
|                 } | ||||
|             } else if (mode == CHORD) { | ||||
|                 add_edge(&e[n++], sx, sy, lx, ly); | ||||
|                 if (inner) { | ||||
|                     add_edge(&e[n++], sx_inner, sy_inner, lx_inner, ly_inner); | ||||
|                 } | ||||
|             } else if (mode == ARC) { | ||||
|                 add_edge(&e[n++], sx, sy, sx_inner, sy_inner); | ||||
|                 add_edge(&e[n++], lx, ly, lx_inner, ly_inner); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         draw->polygon(im, n, e, ink, 0); | ||||
| 
 | ||||
|         free(e); | ||||
|         s->cx = nx; | ||||
|         s->cy = ny; | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| int | ||||
| ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, | ||||
|                float start, float end, const void* ink, int width, int op) | ||||
| { | ||||
|     return ellipse(im, x0, y0, x1, y1, start, end, ink, 0, width, ARC, op); | ||||
| // quarter_* stuff can "draw" a quarter of an ellipse with thickness 1, great.
 | ||||
| // Now we use ellipse_* stuff to join all four quarters of two different sized
 | ||||
| // ellipses and receive horizontal segments of a complete ellipse with
 | ||||
| // specified thickness.
 | ||||
| //
 | ||||
| // Still using integer grid with step 2 at this point (like in quarter_*)
 | ||||
| // to ease angle clipping in future.
 | ||||
| 
 | ||||
| typedef struct { | ||||
|     quarter_state st_o, st_i; | ||||
|     int32_t py, pl, pr; | ||||
|     int32_t cy[4], cl[4], cr[4]; | ||||
|     int8_t bufcnt; | ||||
|     int8_t finished; | ||||
|     int8_t leftmost; | ||||
| } ellipse_state; | ||||
| 
 | ||||
| void ellipse_init(ellipse_state* s, int32_t a, int32_t b, int32_t w) { | ||||
|     s->bufcnt = 0; | ||||
|     s->leftmost = a % 2; | ||||
|     quarter_init(&s->st_o, a, b); | ||||
|     if (w < 1 || quarter_next(&s->st_o, &s->pr, &s->py) == -1) { | ||||
|         s->finished = 1; | ||||
|     } else { | ||||
|         s->finished = 0; | ||||
|         quarter_init(&s->st_i, a - 2 * (w - 1), b - 2 * (w - 1)); | ||||
|         s->pl = s->leftmost; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| int | ||||
| ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, | ||||
|                  float start, float end, const void* ink, int fill, | ||||
|                  int width, int op) | ||||
| int8_t ellipse_next(ellipse_state* s, int32_t* ret_x0, int32_t* ret_y, int32_t* ret_x1) { | ||||
|     if (s->bufcnt == 0) { | ||||
|         if (s->finished) { | ||||
|             return -1; | ||||
|         } | ||||
|         int32_t y = s->py; | ||||
|         int32_t l = s->pl; | ||||
|         int32_t r = s->pr; | ||||
|         int32_t cx = 0, cy = 0; | ||||
|         int8_t next_ret; | ||||
|         while ((next_ret = quarter_next(&s->st_o, &cx, &cy)) != -1 && cy <= y) { | ||||
|         } | ||||
|         if (next_ret == -1) { | ||||
|             s->finished = 1; | ||||
|         } else { | ||||
|             s->pr = cx; | ||||
|             s->py = cy; | ||||
|         } | ||||
|         while ((next_ret = quarter_next(&s->st_i, &cx, &cy)) != -1 && cy <= y) { | ||||
|             l = cx; | ||||
|         } | ||||
|         s->pl = next_ret == -1 ? s->leftmost : cx; | ||||
| 
 | ||||
|         if ((l > 0 || l < r) && y > 0) { | ||||
|             s->cl[s->bufcnt] = l == 0 ? 2 : l; | ||||
|             s->cy[s->bufcnt] = y; | ||||
|             s->cr[s->bufcnt] = r; | ||||
|             ++s->bufcnt; | ||||
|         } | ||||
|         if (y > 0) { | ||||
|             s->cl[s->bufcnt] = -r; | ||||
|             s->cy[s->bufcnt] = y; | ||||
|             s->cr[s->bufcnt] = -l; | ||||
|             ++s->bufcnt; | ||||
|         } | ||||
|         if (l > 0 || l < r) { | ||||
|             s->cl[s->bufcnt] = l == 0 ? 2 : l; | ||||
|             s->cy[s->bufcnt] = -y; | ||||
|             s->cr[s->bufcnt] = r; | ||||
|             ++s->bufcnt; | ||||
|         } | ||||
|         s->cl[s->bufcnt] = -r; | ||||
|         s->cy[s->bufcnt] = -y; | ||||
|         s->cr[s->bufcnt] = -l; | ||||
|         ++s->bufcnt; | ||||
|     } | ||||
|     --s->bufcnt; | ||||
|     *ret_x0 = s->cl[s->bufcnt]; | ||||
|     *ret_y = s->cy[s->bufcnt]; | ||||
|     *ret_x1 = s->cr[s->bufcnt]; | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| // Clipping tree consists of half-plane clipping nodes and combining nodes.
 | ||||
| // We can throw a horizontal segment in such a tree and collect an ordered set
 | ||||
| // of resulting disjoint clipped segments organized into a sorted linked list
 | ||||
| // of their end points.
 | ||||
| typedef enum { | ||||
|   CT_AND, // intersection
 | ||||
|   CT_OR, // union
 | ||||
|   CT_CLIP // half-plane clipping
 | ||||
| } clip_type; | ||||
| 
 | ||||
| typedef struct clip_node { | ||||
|   clip_type type; | ||||
|   double a, b, c; // half-plane coeffs, only used in clipping nodes
 | ||||
|   struct clip_node* l; // child pointers, are only non-NULL in combining nodes
 | ||||
|   struct clip_node* r; | ||||
| } clip_node; | ||||
| 
 | ||||
| // Linked list for the ends of the clipped horizontal segments.
 | ||||
| // Since the segment is always horizontal, we don't need to store Y coordinate.
 | ||||
| typedef struct event_list { | ||||
|   int32_t x; | ||||
|   int8_t type; // used internally, 1 for the left end (smaller X), -1 for the
 | ||||
|                // right end; pointless in output since the output segments
 | ||||
|                // are disjoint, therefore the types would always come in pairs
 | ||||
|                // and interchange (1 -1 1 -1 ...)
 | ||||
|   struct event_list* next; | ||||
| } event_list; | ||||
| 
 | ||||
| // Mirrors all the clipping nodes of the tree relative to the y = x line.
 | ||||
| void clip_tree_transpose(clip_node* root) { | ||||
|   if (root != NULL) { | ||||
|     if (root->type == CT_CLIP) { | ||||
|       double t = root->a; | ||||
|       root->a = root->b; | ||||
|       root->b = t; | ||||
|     } | ||||
|     clip_tree_transpose(root->l); | ||||
|     clip_tree_transpose(root->r); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Outputs a sequence of open-close events (types -1 and 1) for
 | ||||
| // non-intersecting segments sorted by X coordinate.
 | ||||
| // Combining nodes (AND, OR) may also accept sequences for intersecting
 | ||||
| // segments, i.e. something like correct bracket sequences.
 | ||||
| int clip_tree_do_clip(clip_node* root, int32_t x0, int32_t y, int32_t x1, event_list** ret) { | ||||
|   if (root == NULL) { | ||||
|     event_list* start = malloc(sizeof(event_list)); | ||||
|     if (!start) { | ||||
|       ImagingError_MemoryError(); | ||||
|       return -1; | ||||
|     } | ||||
|     event_list* end = malloc(sizeof(event_list)); | ||||
|     if (!end) { | ||||
|       free(start); | ||||
|       ImagingError_MemoryError(); | ||||
|       return -1; | ||||
|     } | ||||
|     start->x = x0; | ||||
|     start->type = 1; | ||||
|     start->next = end; | ||||
|     end->x = x1; | ||||
|     end->type = -1; | ||||
|     end->next = NULL; | ||||
|     *ret = start; | ||||
|     return 0; | ||||
|   } | ||||
|   if (root->type == CT_CLIP) { | ||||
|     double eps = 1e-9; | ||||
|     double A = root->a; | ||||
|     double B = root->b; | ||||
|     double C = root->c; | ||||
|     if (fabs(A) < eps) { | ||||
|       if (B * y + C < -eps) { | ||||
|         x0 = 1; | ||||
|         x1 = 0; | ||||
|       } | ||||
|     } else { | ||||
|       // X of intersection
 | ||||
|       double ix = - (B * y + C) / A; | ||||
|       if (A * x0 + B * y + C < eps) { | ||||
|         x0 = lround(fmax(x0, ix)); | ||||
|       } | ||||
|       if (A * x1 + B * y + C < eps) { | ||||
|         x1 = lround(fmin(x1, ix)); | ||||
|       } | ||||
|     } | ||||
|     if (x0 <= x1) { | ||||
|       event_list* start = malloc(sizeof(event_list)); | ||||
|       if (!start) { | ||||
|         ImagingError_MemoryError(); | ||||
|         return -1; | ||||
|       } | ||||
|       event_list* end = malloc(sizeof(event_list)); | ||||
|       if (!end) { | ||||
|         free(start); | ||||
|         ImagingError_MemoryError(); | ||||
|         return -1; | ||||
|       } | ||||
|       start->x = x0; | ||||
|       start->type = 1; | ||||
|       start->next = end; | ||||
|       end->x = x1; | ||||
|       end->type = -1; | ||||
|       end->next = NULL; | ||||
|       *ret = start; | ||||
|     } else { | ||||
|       *ret = NULL; | ||||
|     } | ||||
|     return 0; | ||||
|   } | ||||
|   if (root->type == CT_OR || root->type == CT_AND) { | ||||
|     event_list* l1; | ||||
|     event_list* l2; | ||||
|     if (clip_tree_do_clip(root->l, x0, y, x1, &l1) < 0) { | ||||
|       return -1; | ||||
|     } | ||||
|     if (clip_tree_do_clip(root->r, x0, y, x1, &l2) < 0) { | ||||
|       while (l1) { | ||||
|         l2 = l1->next; | ||||
|         free(l1); | ||||
|         l1 = l2; | ||||
|       } | ||||
|       return -1; | ||||
|     } | ||||
|     *ret = NULL; | ||||
|     event_list* tail = NULL; | ||||
|     int32_t k1 = 0; | ||||
|     int32_t k2 = 0; | ||||
|     while (l1 != NULL || l2 != NULL) { | ||||
|       event_list* t; | ||||
|       if (l2 == NULL || (l1 != NULL && (l1->x < l2->x || (l1->x == l2->x && l1->type > l2->type)))) { | ||||
|         t = l1; | ||||
|         k1 += t->type; | ||||
|         assert(k1 >= 0); | ||||
|         l1 = l1->next; | ||||
|       } else { | ||||
|         t = l2; | ||||
|         k2 += t->type; | ||||
|         assert(k2 >= 0); | ||||
|         l2 = l2->next; | ||||
|       } | ||||
|       t->next = NULL; | ||||
|       if ((root->type == CT_OR && ( | ||||
|               (t->type == 1 && (tail == NULL || tail->type == -1)) || | ||||
|               (t->type == -1 && k1 == 0 && k2 == 0) | ||||
|           )) ||  | ||||
|           (root->type == CT_AND && ( | ||||
|               (t->type == 1 && (tail == NULL || tail->type == -1) && k1 > 0 && k2 > 0) || | ||||
|               (t->type == -1 && tail != NULL && tail->type == 1 && (k1 == 0 || k2 == 0)) | ||||
|           ))) { | ||||
|         if (tail == NULL) { | ||||
|           *ret = t; | ||||
|         } else { | ||||
|           tail->next = t; | ||||
|         } | ||||
|         tail = t; | ||||
|       } else { | ||||
|         free(t); | ||||
|       } | ||||
|     } | ||||
|     return 0; | ||||
|   } | ||||
|   *ret = NULL; | ||||
|   return 0; | ||||
| } | ||||
| 
 | ||||
| // One more layer of processing on top of the regular ellipse.
 | ||||
| // Uses the clipping tree.
 | ||||
| // Used for producing ellipse derivatives such as arc, chord, pie, etc.
 | ||||
| typedef struct { | ||||
|   ellipse_state st; | ||||
|   clip_node* root; | ||||
|   clip_node nodes[7]; | ||||
|   int32_t node_count; | ||||
|   event_list* head; | ||||
|   int32_t y; | ||||
| } clip_ellipse_state; | ||||
| 
 | ||||
| typedef void (*clip_ellipse_init)(clip_ellipse_state*, int32_t, int32_t, int32_t, float, float); | ||||
| 
 | ||||
| void debug_clip_tree(clip_node* root, int space) { | ||||
|   if (root == NULL) { | ||||
|     return; | ||||
|   } | ||||
|   if (root->type == CT_CLIP) { | ||||
|     int t = space; | ||||
|     while (t--) { | ||||
|       fputc(' ', stderr); | ||||
|     } | ||||
|     fprintf(stderr, "clip %+fx%+fy%+f > 0\n", root->a, root->b, root->c); | ||||
|   } else { | ||||
|     debug_clip_tree(root->l, space + 2); | ||||
|     int t = space; | ||||
|     while (t--) { | ||||
|       fputc(' ', stderr); | ||||
|     } | ||||
|     fprintf(stderr, "%s\n", root->type == CT_AND ? "and" : "or"); | ||||
|     debug_clip_tree(root->r, space + 2); | ||||
|   } | ||||
|   if (space == 0) { | ||||
|     fputc('\n', stderr); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // Resulting angles will satisfy 0 <= al < 360, al <= ar <= al + 360
 | ||||
| void normalize_angles(float* al, float* ar) { | ||||
|   if (*ar - *al >= 360) { | ||||
|     *al = 0; | ||||
|     *ar = 360; | ||||
|   } else { | ||||
|     *al = fmod(*al < 0 ? 360 - (fmod(-*al, 360)) : *al, 360); | ||||
|     *ar = *al + fmod(*ar < *al ? 360 - fmod(*al - *ar, 360) : *ar - *al, 360); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // An arc with caps orthogonal to the ellipse curve.
 | ||||
| void arc_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { | ||||
|   if (a < b) { | ||||
|     // transpose the coordinate system
 | ||||
|     arc_init(s, b, a, w, 90 - ar, 90 - al); | ||||
|     ellipse_init(&s->st, a, b, w); | ||||
|     clip_tree_transpose(s->root); | ||||
|   } else { | ||||
|     // a >= b, based on "wide" ellipse
 | ||||
|     ellipse_init(&s->st, a, b, w); | ||||
| 
 | ||||
|     s->head = NULL; | ||||
|     s->node_count = 0; | ||||
|     normalize_angles(&al, &ar); | ||||
| 
 | ||||
|     // building clipping tree, a lot of different cases
 | ||||
|     if (ar == al + 360) { | ||||
|       s->root = NULL; | ||||
|     } else { | ||||
|       clip_node* lc = s->nodes + s->node_count++; | ||||
|       clip_node* rc = s->nodes + s->node_count++; | ||||
|       lc->l = lc->r = rc->l = rc->r = NULL; | ||||
|       lc->type = rc->type = CT_CLIP; | ||||
|       lc->a = -a * sin(al * M_PI / 180.0); | ||||
|       lc->b = b * cos(al * M_PI / 180.0); | ||||
|       lc->c = (a * a - b * b) * sin(al * M_PI / 90.0) / 2.0; | ||||
|       rc->a = a * sin(ar * M_PI / 180.0); | ||||
|       rc->b = -b * cos(ar * M_PI / 180.0); | ||||
|       rc->c = (b * b - a * a) * sin(ar * M_PI / 90.0) / 2.0; | ||||
|       if (fmod(al, 180) == 0 || fmod(ar, 180) == 0) { | ||||
|         s->root = s->nodes + s->node_count++; | ||||
|         s->root->l = lc; | ||||
|         s->root->r = rc; | ||||
|         s->root->type = ar - al < 180 ? CT_AND : CT_OR; | ||||
|       } else if (((int)(al / 180) + (int)(ar / 180)) % 2 == 1) { | ||||
|         s->root = s->nodes + s->node_count++; | ||||
|         s->root->l = s->nodes + s->node_count++; | ||||
|         s->root->l->l = s->nodes + s->node_count++; | ||||
|         s->root->l->r = lc; | ||||
|         s->root->r = s->nodes + s->node_count++; | ||||
|         s->root->r->l = s->nodes + s->node_count++; | ||||
|         s->root->r->r = rc; | ||||
|         s->root->type = CT_OR; | ||||
|         s->root->l->type = CT_AND; | ||||
|         s->root->r->type = CT_AND; | ||||
|         s->root->l->l->type = CT_CLIP; | ||||
|         s->root->r->l->type = CT_CLIP; | ||||
|         s->root->l->l->l = s->root->l->l->r = NULL; | ||||
|         s->root->r->l->l = s->root->r->l->r = NULL; | ||||
|         s->root->l->l->a = s->root->l->l->c = 0; | ||||
|         s->root->r->l->a = s->root->r->l->c = 0; | ||||
|         s->root->l->l->b = (int)(al / 180) % 2 == 0 ? 1 : -1; | ||||
|         s->root->r->l->b = (int)(ar / 180) % 2 == 0 ? 1 : -1; | ||||
|       } else { | ||||
|         s->root = s->nodes + s->node_count++; | ||||
|         s->root->l = s->nodes + s->node_count++; | ||||
|         s->root->r = s->nodes + s->node_count++; | ||||
|         s->root->type = s->root->l->type = ar - al < 180 ? CT_AND : CT_OR; | ||||
|         s->root->l->l = lc; | ||||
|         s->root->l->r = rc; | ||||
|         s->root->r->type = CT_CLIP; | ||||
|         s->root->r->l = s->root->r->r = NULL; | ||||
|         s->root->r->a = s->root->r->c = 0; | ||||
|         s->root->r->b = ar < 180 || ar > 540 ? 1 : -1; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // A chord line.
 | ||||
| void chord_line_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { | ||||
|   ellipse_init(&s->st, a, b, a + b + 1); | ||||
| 
 | ||||
|   s->head = NULL; | ||||
|   s->node_count = 0; | ||||
| 
 | ||||
|   // line equation for chord
 | ||||
|   double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); | ||||
|   double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); | ||||
|   s->root = s->nodes + s->node_count++; | ||||
|   s->root->l = s->nodes + s->node_count++; | ||||
|   s->root->r = s->nodes + s->node_count++; | ||||
|   s->root->type = CT_AND; | ||||
|   s->root->l->type = s->root->r->type = CT_CLIP; | ||||
|   s->root->l->l = s->root->l->r = s->root->r->l = s->root->r->r = NULL; | ||||
|   s->root->l->a = yr - yl; | ||||
|   s->root->l->b = xl - xr; | ||||
|   s->root->l->c = -(s->root->l->a * xl + s->root->l->b * yl); | ||||
|   s->root->r->a = -s->root->l->a; | ||||
|   s->root->r->b = -s->root->l->b; | ||||
|   s->root->r->c = 2 * w * sqrt(pow(s->root->l->a, 2.0) + pow(s->root->l->b, 2.0)) - s->root->l->c; | ||||
| } | ||||
| 
 | ||||
| // Pie side.
 | ||||
| void pie_side_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float _) { | ||||
|   ellipse_init(&s->st, a, b, a + b + 1); | ||||
| 
 | ||||
|   s->head = NULL; | ||||
|   s->node_count = 0; | ||||
| 
 | ||||
|   double xl = a * cos(al * M_PI / 180.0); | ||||
|   double yl = b * sin(al * M_PI / 180.0); | ||||
|   double a1 = -yl; | ||||
|   double b1 = xl; | ||||
|   double c1 = w * sqrt(a1 * a1 + b1 * b1); | ||||
| 
 | ||||
|   s->root = s->nodes + s->node_count++; | ||||
|   s->root->type = CT_AND; | ||||
|   s->root->l = s->nodes + s->node_count++; | ||||
|   s->root->l->type = CT_AND; | ||||
| 
 | ||||
|   clip_node* cnode; | ||||
|   cnode = s->nodes + s->node_count++; | ||||
|   cnode->l = cnode->r = NULL; | ||||
|   cnode->type = CT_CLIP; | ||||
|   cnode->a = a1; | ||||
|   cnode->b = b1; | ||||
|   cnode->c = c1; | ||||
|   s->root->l->l = cnode; | ||||
|   cnode = s->nodes + s->node_count++; | ||||
|   cnode->l = cnode->r = NULL; | ||||
|   cnode->type = CT_CLIP; | ||||
|   cnode->a = -a1; | ||||
|   cnode->b = -b1; | ||||
|   cnode->c = c1; | ||||
|   s->root->l->r = cnode; | ||||
|   cnode = s->nodes + s->node_count++; | ||||
|   cnode->l = cnode->r = NULL; | ||||
|   cnode->type = CT_CLIP; | ||||
|   cnode->a = b1; | ||||
|   cnode->b = -a1; | ||||
|   cnode->c = 0; | ||||
|   s->root->r = cnode; | ||||
| } | ||||
| 
 | ||||
| // A chord.
 | ||||
| void chord_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { | ||||
|   ellipse_init(&s->st, a, b, w); | ||||
| 
 | ||||
|   s->head = NULL; | ||||
|   s->node_count = 0; | ||||
| 
 | ||||
|   // line equation for chord
 | ||||
|   double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); | ||||
|   double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); | ||||
|   s->root = s->nodes + s->node_count++; | ||||
|   s->root->l = s->root->r = NULL; | ||||
|   s->root->type = CT_CLIP; | ||||
|   s->root->a = yr - yl; | ||||
|   s->root->b = xl - xr; | ||||
|   s->root->c = -(s->root->a * xl + s->root->b * yl); | ||||
| } | ||||
| 
 | ||||
| // A pie. Can also be used to draw an arc with ugly sharp caps.
 | ||||
| void pie_init(clip_ellipse_state* s, int32_t a, int32_t b, int32_t w, float al, float ar) { | ||||
|   ellipse_init(&s->st, a, b, w); | ||||
| 
 | ||||
|   s->head = NULL; | ||||
|   s->node_count = 0; | ||||
| 
 | ||||
|   // line equations for pie sides
 | ||||
|   double xl = a * cos(al * M_PI / 180.0), xr = a * cos(ar * M_PI / 180.0); | ||||
|   double yl = b * sin(al * M_PI / 180.0), yr = b * sin(ar * M_PI / 180.0); | ||||
| 
 | ||||
|   clip_node* lc = s->nodes + s->node_count++; | ||||
|   clip_node* rc = s->nodes + s->node_count++; | ||||
|   lc->l = lc->r = rc->l = rc->r = NULL; | ||||
|   lc->type = rc->type = CT_CLIP; | ||||
|   lc->a = -yl; | ||||
|   lc->b = xl; | ||||
|   lc->c = 0; | ||||
|   rc->a = yr; | ||||
|   rc->b = -xr; | ||||
|   rc->c = 0; | ||||
|          | ||||
|   s->root = s->nodes + s->node_count++; | ||||
|   s->root->l = lc; | ||||
|   s->root->r = rc; | ||||
|   s->root->type = ar - al < 180 ? CT_AND : CT_OR; | ||||
| } | ||||
| 
 | ||||
| void clip_ellipse_free(clip_ellipse_state* s) { | ||||
|   while (s->head != NULL) { | ||||
|     event_list* t = s->head; | ||||
|     s->head = s->head->next; | ||||
|     free(t); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| int8_t clip_ellipse_next(clip_ellipse_state* s, int32_t* ret_x0, int32_t* ret_y, int32_t* ret_x1) { | ||||
|   int32_t x0, y, x1; | ||||
|   while (s->head == NULL && ellipse_next(&s->st, &x0, &y, &x1) >= 0) { | ||||
|     if (clip_tree_do_clip(s->root, x0, y, x1, &s->head) < 0) { | ||||
|       return -2; | ||||
|     } | ||||
|     s->y = y; | ||||
|   } | ||||
|   if (s->head != NULL) { | ||||
|     *ret_y = s->y; | ||||
|     event_list* t = s->head; | ||||
|     s->head = s->head->next; | ||||
|     *ret_x0 = t->x; | ||||
|     free(t); | ||||
|     t = s->head; | ||||
|     assert(t != NULL); | ||||
|     s->head = s->head->next; | ||||
|     *ret_x1 = t->x; | ||||
|     free(t); | ||||
|     return 0; | ||||
|   } | ||||
|   return -1; | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| ellipseNew(Imaging im, int x0, int y0, int x1, int y1, | ||||
|         const void* ink_, int fill, | ||||
|         int width, int op) | ||||
| { | ||||
|     return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, width, CHORD, op); | ||||
|     DRAW* draw; | ||||
|     INT32 ink; | ||||
|     DRAWINIT(); | ||||
| 
 | ||||
|     int a = x1 - x0; | ||||
|     int b = y1 - y0; | ||||
|     if (a < 0 || b < 0) { | ||||
|       return 0; | ||||
|     } | ||||
|     if (fill) { | ||||
|       width = a + b; | ||||
|     } | ||||
| 
 | ||||
|     ellipse_state st; | ||||
|     ellipse_init(&st, a, b, width); | ||||
|     int32_t X0, Y, X1; | ||||
|     while (ellipse_next(&st, &X0, &Y, &X1) != -1) { | ||||
|       draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); | ||||
|     } | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| clipEllipseNew(Imaging im, int x0, int y0, int x1, int y1, | ||||
|         float start, float end, | ||||
|         const void* ink_, int width, int op, clip_ellipse_init init) | ||||
| { | ||||
|     DRAW* draw; | ||||
|     INT32 ink; | ||||
|     DRAWINIT(); | ||||
| 
 | ||||
|     int a = x1 - x0; | ||||
|     int b = y1 - y0; | ||||
|     if (a < 0 || b < 0) { | ||||
|       return 0; | ||||
|     } | ||||
| 
 | ||||
|     clip_ellipse_state st; | ||||
|     init(&st, a, b, width, start, end); | ||||
|     // debug_clip_tree(st.root, 0);
 | ||||
|     int32_t X0, Y, X1; | ||||
|     int next_code; | ||||
|     while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) { | ||||
|       draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink); | ||||
|     } | ||||
|     clip_ellipse_free(&st); | ||||
|     return next_code == -1 ? 0 : -1; | ||||
| } | ||||
| static int | ||||
| arcNew(Imaging im, int x0, int y0, int x1, int y1, | ||||
|         float start, float end, | ||||
|         const void* ink_, int width, int op) | ||||
| { | ||||
|   return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, arc_init); | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| chordNew(Imaging im, int x0, int y0, int x1, int y1, | ||||
|         float start, float end, | ||||
|         const void* ink_, int width, int op) | ||||
| { | ||||
|   return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_init); | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| chordLineNew(Imaging im, int x0, int y0, int x1, int y1, | ||||
|         float start, float end, | ||||
|         const void* ink_, int width, int op) | ||||
| { | ||||
|   return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, chord_line_init); | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| pieNew(Imaging im, int x0, int y0, int x1, int y1, | ||||
|         float start, float end, | ||||
|         const void* ink_, int width, int op) | ||||
| { | ||||
|   return clipEllipseNew(im, x0, y0, x1, y1, start, end, ink_, width, op, pie_init); | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| pieSideNew(Imaging im, int x0, int y0, int x1, int y1, | ||||
|         float start, | ||||
|         const void* ink_, int width, int op) | ||||
| { | ||||
|   return clipEllipseNew(im, x0, y0, x1, y1, start, 0, ink_, width, op, pie_side_init); | ||||
| } | ||||
| 
 | ||||
| int | ||||
| ImagingDrawEllipse(Imaging im, int x0, int y0, int x1, int y1, | ||||
|                    const void* ink, int fill, int width, int op) | ||||
| { | ||||
|     return ellipse(im, x0, y0, x1, y1, 0, 360, ink, fill, width, CHORD, op); | ||||
|     return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); | ||||
| } | ||||
| 
 | ||||
| int | ||||
| ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, | ||||
|                float start, float end, const void* ink, int width, int op) | ||||
| { | ||||
|     normalize_angles(&start, &end); | ||||
|     if (start + 360 == end) { | ||||
|       return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, 0, width, op); | ||||
|     } | ||||
|     if (start == end) { | ||||
|       return 0; | ||||
|     } | ||||
|     return arcNew(im, x0, y0, x1, y1, start, end, ink, width, op); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| int | ||||
| ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, | ||||
|                  float start, float end, const void* ink, int fill, | ||||
|                  int width, int op) | ||||
| { | ||||
|     normalize_angles(&start, &end); | ||||
|     if (start + 360 == end) { | ||||
|       return ImagingDrawEllipse(im, x0, y0, x1, y1, ink, fill, width, op); | ||||
|     } | ||||
|     if (start == end) { | ||||
|       return 0; | ||||
|     } | ||||
|     if (fill) { | ||||
|       return chordNew(im, x0, y0, x1, y1, start, end, ink, x1 - x0 + y1 - y0 + 1, op); | ||||
|     } else { | ||||
|       if (chordLineNew(im, x0, y0, x1, y1, start, end, ink, width, op)) { | ||||
|         return -1; | ||||
|       } | ||||
|       return chordNew(im, x0, y0, x1, y1, start, end, ink, width, op); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| int | ||||
| ImagingDrawPieslice(Imaging im, int x0, int y0, int x1, int y1, | ||||
|                     float start, float end, const void* ink, int fill, | ||||
|                     int width, int op) | ||||
| { | ||||
|     return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, width, PIESLICE, op); | ||||
|     normalize_angles(&start, &end); | ||||
|     if (start + 360 == end) { | ||||
|       return ellipseNew(im, x0, y0, x1, y1, ink, fill, width, op); | ||||
|     } | ||||
|     if (start == end) { | ||||
|       return 0; | ||||
|     } | ||||
|     if (fill) { | ||||
|       return pieNew(im, x0, y0, x1, y1, start, end, ink, x1 + y1 - x0 - y0, op); | ||||
|     } else { | ||||
|       if (pieSideNew(im, x0, y0, x1, y1, start, ink, width, op)) { | ||||
|         return -1; | ||||
|       } | ||||
|       if (pieSideNew(im, x0, y0, x1, y1, end, ink, width, op)) { | ||||
|         return -1; | ||||
|       } | ||||
|       int xc = lround((x0 + x1 - width) / 2.0), yc = lround((y0 + y1 - width) / 2.0); | ||||
|       ellipseNew(im, xc, yc, xc + width - 1, yc + width - 1, ink, 1, 0, op); | ||||
|       return pieNew(im, x0, y0, x1, y1, start, end, ink, width, op); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* -------------------------------------------------------------------- */ | ||||
| 
 | ||||
| /* experimental level 2 ("arrow") graphics stuff.  this implements
 | ||||
|  |  | |||
|  | @ -169,6 +169,20 @@ deps = { | |||
|         ], | ||||
|         "libs": [r"output\release-static\{architecture}\lib\*.lib"], | ||||
|     }, | ||||
|     "libpng": { | ||||
|         "url": SF_MIRROR + "/project/libpng/libpng16/1.6.37/lpng1637.zip", | ||||
|         "filename": "lpng1637.zip", | ||||
|         "dir": "lpng1637", | ||||
|         "build": [ | ||||
|             # lint: do not inline | ||||
|             cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), | ||||
|             cmd_nmake(target="clean"), | ||||
|             cmd_nmake(), | ||||
|             cmd_copy("libpng16_static.lib", "libpng16.lib"), | ||||
|         ], | ||||
|         "headers": [r"png*.h"], | ||||
|         "libs": [r"libpng16.lib"], | ||||
|     }, | ||||
|     "freetype": { | ||||
|         "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.2.tar.gz",  # noqa: E501 | ||||
|         "filename": "freetype-2.10.2.tar.gz", | ||||
|  | @ -181,8 +195,10 @@ deps = { | |||
|                 '<PropertyGroup Label="Globals">': '<PropertyGroup Label="Globals">\n    <WindowsTargetPlatformVersion>$(WindowsSDKVersion)</WindowsTargetPlatformVersion>',  # noqa: E501 | ||||
|             }, | ||||
|             r"builds\windows\vc2010\freetype.user.props": { | ||||
|                 "<UserDefines></UserDefines>": "<UserDefines>FT_CONFIG_OPTION_USE_HARFBUZZ</UserDefines>",  # noqa: E501 | ||||
|                 "<UserIncludeDirectories></UserIncludeDirectories>": r"<UserIncludeDirectories>{dir_harfbuzz}\src</UserIncludeDirectories>",  # noqa: E501 | ||||
|                 "<UserDefines></UserDefines>": "<UserDefines>FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ</UserDefines>",  # noqa: E501 | ||||
|                 "<UserIncludeDirectories></UserIncludeDirectories>": r"<UserIncludeDirectories>{dir_harfbuzz}\src;{inc_dir}</UserIncludeDirectories>",  # noqa: E501 | ||||
|                 "<UserLibraryDirectories></UserLibraryDirectories>": "<UserLibraryDirectories>{lib_dir}</UserLibraryDirectories>",  # noqa: E501 | ||||
|                 "<UserDependencies></UserDependencies>": "<UserDependencies>zlib.lib;libpng16.lib</UserDependencies>",  # noqa: E501 | ||||
|             }, | ||||
|             r"src/autofit/afshaper.c": { | ||||
|                 # link against harfbuzz.lib once it becomes available | ||||
|  |  | |||