From dc4906c8651c4f007712174034779e26c556592c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 May 2022 22:50:30 +1000 Subject: [PATCH 001/242] Updated codecov action to v3 --- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 497b994db28..7cd8922194e 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -75,7 +75,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 71b54021c3b..c2456d21894 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -168,7 +168,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: file: ./coverage.xml flags: GHA_Windows From a48d95061da0a7e23d24b7b8501a90a9fd027e52 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Jun 2022 16:00:31 +1000 Subject: [PATCH 002/242] Use gnome-screenshot on Linux if available --- src/PIL/ImageGrab.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index eb21ac39948..54e01aa03e9 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,15 +15,14 @@ # See the README file for information on usage and redistribution. # +import os +import shutil +import subprocess import sys +import tempfile from . import Image -if sys.platform == "darwin": - import os - import subprocess - import tempfile - def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None): if xdisplay is None: @@ -62,6 +61,18 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N left, top, right, bottom = bbox im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im + elif not Image.core.HAVE_XCB and shutil.which("gnome-screenshot"): + fh, filepath = tempfile.mkstemp(".png") + os.close(fh) + subprocess.call(["gnome-screenshot", "-f", filepath]) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + if bbox: + im_cropped = im.crop(bbox) + im.close() + return im_cropped + return im # use xdisplay=None for default display on non-win32/macOS systems if not Image.core.HAVE_XCB: raise OSError("Pillow was built without XCB support") From b1ba0909edb8f4c2c7815397c5e39c6a36e3bbb3 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 13 Jun 2022 09:56:52 +1000 Subject: [PATCH 003/242] Prefer gnome-screenshot if xdisplay is None MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 54e01aa03e9..38074cb1b0d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -61,7 +61,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N left, top, right, bottom = bbox im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im - elif not Image.core.HAVE_XCB and shutil.which("gnome-screenshot"): + elif shutil.which("gnome-screenshot"): fh, filepath = tempfile.mkstemp(".png") os.close(fh) subprocess.call(["gnome-screenshot", "-f", filepath]) From 463d18191a5f3e41003f2dc1530dd1a9b139dacc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Jun 2022 10:16:30 +1000 Subject: [PATCH 004/242] Document use of gnome-screenshot --- docs/reference/ImageGrab.rst | 5 ++++- docs/releasenotes/9.2.0.rst | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index ac83b225522..3086ba8c311 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -15,7 +15,10 @@ or the clipboard to a PIL image memory. returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, the entire screen is copied. - .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux (X11)) + On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it + is installed. To capture the default X11 display instead, pass ``xdisplay=""``. + + .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used. diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 424fd487a29..20e6cfa950f 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -76,7 +76,9 @@ TODO Other Changes ============= -TODO -^^^^ +Using gnome-screenshot on Linux +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +In :py:meth:`~PIL.ImageGrab.grab` on Linux, if ``xdisplay`` is ``None`` then +``gnome-screenshot`` will be used to capture the display if it is installed. To capture +the default X11 display instead, pass ``xdisplay=""``. From b47bcc246ff5506bec4f3b0079bb28c65e792966 Mon Sep 17 00:00:00 2001 From: Jingxuan He Date: Wed, 15 Jun 2022 17:39:37 +0200 Subject: [PATCH 005/242] Fix a potential wrong operator bug --- src/PIL/ImageCms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 50ec3b5efa3..282c314201a 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -377,7 +377,7 @@ def profileToProfile( raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError("flags must be an integer between 0 and %s" % _MAX_FLAG) try: if not isinstance(inputProfile, ImageCmsProfile): From 6c889d10883d264bd946e37ba081631590a44353 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 15 Jun 2022 21:34:16 +0300 Subject: [PATCH 006/242] Test ImageCms.profileToProfile with invalid flags --- Tests/test_imagecms.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 66a72a90eb2..0c0eb92c47e 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -210,6 +210,15 @@ def test_invalid_color_temperature(): ImageCms.createProfile("LAB", "invalid") +@pytest.mark.parametrize("flag", ("my string", -1)) +def test_invalid_flag(flag): + with hopper() as im: + with pytest.raises( + ImageCms.PyCMSError, match="flags must be an integer between 0 and " + ): + ImageCms.profileToProfile(im, "foo", "bar", flags=flag) + + def test_simple_lab(): i = Image.new("RGB", (10, 10), (128, 128, 128)) From 983a6139d57b37a883344972c6b1de50bb757de0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 15 Jun 2022 21:42:04 +0300 Subject: [PATCH 007/242] Check other exception messages --- Tests/test_imagecms.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 0c0eb92c47e..ce6ec0bacf4 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -174,19 +174,25 @@ def test_exceptions(): psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="mode mismatch"): t.apply_in_place(hopper("RGBA")) # the procedural pyCMS API uses PyCMSError for all sorts of errors with hopper() as im: - with pytest.raises(ImageCms.PyCMSError): + with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): ImageCms.profileToProfile(im, "foo", "bar") - with pytest.raises(ImageCms.PyCMSError): + + with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): ImageCms.buildTransform("foo", "bar", "RGB", "RGB") - with pytest.raises(ImageCms.PyCMSError): + + with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"): ImageCms.getProfileName(None) skip_missing() - with pytest.raises(ImageCms.PyCMSError): + + with pytest.raises( + ImageCms.PyCMSError, + match="'NoneType' object cannot be interpreted as an integer", + ): ImageCms.isIntentSupported(SRGB, None, None) @@ -201,12 +207,20 @@ def test_lab_color_profile(): def test_unsupported_color_space(): - with pytest.raises(ImageCms.PyCMSError): + with pytest.raises( + ImageCms.PyCMSError, + match=re.escape( + "Color space not supported for on-the-fly profile creation (unsupported)" + ), + ): ImageCms.createProfile("unsupported") def test_invalid_color_temperature(): - with pytest.raises(ImageCms.PyCMSError): + with pytest.raises( + ImageCms.PyCMSError, + match='Color temperature must be numeric, "invalid" not valid', + ): ImageCms.createProfile("LAB", "invalid") @@ -470,9 +484,9 @@ def test_profile_typesafety(): prepatch, these would segfault, postpatch they should emit a typeerror """ - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Invalid type for Profile"): ImageCms.ImageCmsProfile(0).tobytes() - with pytest.raises(TypeError): + with pytest.raises(TypeError, match="Invalid type for Profile"): ImageCms.ImageCmsProfile(1).tobytes() From 959e576dd2a8fd86d6bc0b5ebebdb7f73daef829 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 15 Jun 2022 21:43:18 +0300 Subject: [PATCH 008/242] Use f-string --- src/PIL/ImageCms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 282c314201a..605252d5d4c 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -377,7 +377,7 @@ def profileToProfile( raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" % _MAX_FLAG) + raise PyCMSError(f"flags must be an integer between 0 and {_MAX_FLAG}") try: if not isinstance(inputProfile, ImageCmsProfile): From 6ae6a241f29b8823996b22dde9a516b3e6c560a4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 15 Jun 2022 21:57:20 +0300 Subject: [PATCH 009/242] More generic match text --- Tests/test_imagecms.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index ce6ec0bacf4..6dd38894177 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -189,10 +189,9 @@ def test_exceptions(): ImageCms.getProfileName(None) skip_missing() - with pytest.raises( - ImageCms.PyCMSError, - match="'NoneType' object cannot be interpreted as an integer", - ): + # macOS/Ubuntu: "'NoneType' object cannot be interpreted as an integer" + # Windows: "an integer is required (got type NoneType)" + with pytest.raises(ImageCms.PyCMSError, match="integer"): ImageCms.isIntentSupported(SRGB, None, None) From 4d6e5a0d095cc1567f286e470d7af286a3e8d50e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Jun 2022 08:36:43 +1000 Subject: [PATCH 010/242] Limit tile size to avoid extending outside image --- Tests/images/input_bw_one_band.fpx | Bin 0 -> 33792 bytes Tests/images/input_bw_one_band.png | Bin 0 -> 477 bytes Tests/test_file_fpx.py | 11 +++++++++++ src/PIL/FpxImagePlugin.py | 9 ++++++--- 4 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 Tests/images/input_bw_one_band.fpx create mode 100644 Tests/images/input_bw_one_band.png diff --git a/Tests/images/input_bw_one_band.fpx b/Tests/images/input_bw_one_band.fpx new file mode 100644 index 0000000000000000000000000000000000000000..9bdc53763fe9459ecc35cd45feabf15d06d6d124 GIT binary patch literal 33792 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 literal 0 HcmV?d00001 diff --git a/Tests/images/input_bw_one_band.png b/Tests/images/input_bw_one_band.png new file mode 100644 index 0000000000000000000000000000000000000000..6b4c1f37696a847dc9859da93af8c6e1eec52eb7 GIT binary patch literal 477 zcmV<30V4j1P)jUe z_XDE!potni8@oajKyE?{?8Z%ECs8g#)@9yoW@kcW;BZE9i>uIBPxe}^*M_-BtxHm) z#&%_c(lJ}*c-uLY21#*P)t4(WiBB@7Z5?@LleG0V##ncOo~JrrDW-9e3m8SXEG41k zQ1ooVStIZKToK&(#R4kiwC;vq;Z`ffXh z2d$enH~Se(8Tk+^-q;G|m{7a}n>3st$)Emhao@qT5)o=Y?C2a(a)+nVz8CffiPcI% TjOZMM00000NkvXXu0mjf^4ivS literal 0 HcmV?d00001 diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index 818565f88b3..fa22e90f660 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -2,11 +2,22 @@ from PIL import Image +from .helper import assert_image_equal_tofile + FpxImagePlugin = pytest.importorskip( "PIL.FpxImagePlugin", reason="olefile not installed" ) +def test_sanity(): + with Image.open("Tests/images/input_bw_one_band.fpx") as im: + assert im.mode == "L" + assert im.size == (70, 46) + assert im.format == "FPX" + + assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index f955b234769..a55376d0e08 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -154,13 +154,16 @@ def _open_subimage(self, index=1, subimage=0): for i in range(0, len(s), length): + x1 = min(xsize, x + xtile) + y1 = min(ysize, y + ytile) + compression = i32(s, i + 8) if compression == 0: self.tile.append( ( "raw", - (x, y, x + xtile, y + ytile), + (x, y, x1, y1), i32(s, i) + 28, (self.rawmode,), ) @@ -172,7 +175,7 @@ def _open_subimage(self, index=1, subimage=0): self.tile.append( ( "fill", - (x, y, x + xtile, y + ytile), + (x, y, x1, y1), i32(s, i) + 28, (self.rawmode, s[12:16]), ) @@ -201,7 +204,7 @@ def _open_subimage(self, index=1, subimage=0): self.tile.append( ( "jpeg", - (x, y, x + xtile, y + ytile), + (x, y, x1, y1), i32(s, i) + 28, (rawmode, jpegmode), ) From 765d66c069fb35442a01457955c15a8db898f734 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Jun 2022 19:24:36 +1000 Subject: [PATCH 011/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 658a950dd8b..8d5d7001a86 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Limit FPX tile size to avoid extending outside image #6368 + [radarhere] + - Added support for decoding plain PPM formats #5242 [Piolie, radarhere] From c2047b8293ec9091970c09abb113d5de586d1b7b Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 17 Jun 2022 17:13:11 +0100 Subject: [PATCH 012/242] fix null check for fribidi_version_info in fribidi shim --- src/thirdparty/fribidi-shim/fribidi.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/thirdparty/fribidi-shim/fribidi.c b/src/thirdparty/fribidi-shim/fribidi.c index 04491e17f8b..5663da86b92 100644 --- a/src/thirdparty/fribidi-shim/fribidi.c +++ b/src/thirdparty/fribidi-shim/fribidi.c @@ -33,6 +33,7 @@ static void fribidi_get_bracket_types_compat( int load_fribidi(void) { int error = 0; + const char **p_fribidi_version_info = 0; p_fribidi = 0; @@ -87,20 +88,21 @@ int load_fribidi(void) { LOAD_FUNCTION(fribidi_get_par_embedding_levels); #ifndef _WIN32 - fribidi_version_info = *(const char**)dlsym(p_fribidi, "fribidi_version_info"); - if (error || (fribidi_version_info == 0)) { + p_fribidi_version_info = (const char**)dlsym(p_fribidi, "fribidi_version_info"); + if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) { dlclose(p_fribidi); p_fribidi = 0; return 2; } #else - fribidi_version_info = *(const char**)GetProcAddress(p_fribidi, "fribidi_version_info"); - if (error || (fribidi_version_info == 0)) { + p_fribidi_version_info = (const char**)GetProcAddress(p_fribidi, "fribidi_version_info"); + if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) { FreeLibrary(p_fribidi); p_fribidi = 0; return 2; } #endif + fribidi_version_info = *p_fribidi_version_info; return 0; } From 9f9e26522eddbd4db3012b761ec6754dab5605d5 Mon Sep 17 00:00:00 2001 From: William Jacobs Date: Sat, 18 Jun 2022 04:09:41 -0400 Subject: [PATCH 013/242] Fixed bug with rounding pixels to palette This fixes a bug with rounding pixels to the nearest palette color. Specifically, there is a typo with computing the minimum distance from a palette color to a given cache box. This is causing palette colors to be incorrectly excluded from consideration. --- Tests/images/palette_negative.png | Bin 17070 -> 16984 bytes Tests/images/palette_sepia.png | Bin 20339 -> 20364 bytes Tests/images/palette_wedge.png | Bin 17065 -> 16984 bytes Tests/test_image_entropy.py | 2 +- Tests/test_image_getcolors.py | 2 +- Tests/test_image_histogram.py | 2 +- docs/releasenotes/9.2.0.rst | 8 +++++--- src/libImaging/Palette.c | 6 +++--- 8 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Tests/images/palette_negative.png b/Tests/images/palette_negative.png index 938a7285fd75af8fcdb5e5857e05e9674b9ae337..7fcfd29a0db44940bf69e76a9aa4a06b8e39f660 100644 GIT binary patch literal 16984 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�i8(H2WE)WEyW#bL`2xuYh zbVMEf%;H}&9EQgLl5RB&k*D@o2^_8DhD7V#JHS5SG#vcht+OM&sl@3QY57_@A+F;# z>ueT-YQuNwQTI-_*}K%YK%tjXCE5aYf`0t9>2EUb{cs1O;QjKy+cO+&BVAuoA~ndB zoX84fvn-F}!z71?xC-)q*7-uuD$~{HR$4!J1JVn2QQyMqYA!1MX~S{c*#u0+JanE0 z6$X*%AJiO(sj2B{W7ZpnYRVd5#?e9fDq`3}nUTar1Pp+@O}Xno%m3pecPCBh{ZpHj zu}gyq=zUH?Z?d9`Wr8aWXw%77lzhFs@GT3cNWEb7TBqzyJg@^TmvBT9>6a?{woAXO zDLXe7FMJ0^!8`?f=-40o$%tJ#km#dq98Frs_~AP>x~%}|_wxP$`~YEne^CyNw>r{m zCCrp23(y5xIyw|JxRUd6gxqGk=9WuEGMAY1YX3qlrDICmSruni8$Rg_A5-5CC!g{9 zwRVWz;xkMo1=n|p8&=@O{iO^}og0azRZ9m|iX0l5GG literal 17070 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%0DN|MLC=0`L9H+oFLFil*`C(bS9|Oy`Ir!0kQo zv0;|XewDlmy!9m)GI)rvF8~$1L4hDkv(b*iE6S~fy>85$oD%L39$&6^hisZN^JkDF zLM*8SQ>S+#1?Otb4)l69Une%tE-&ZyWkG160C|vetK6zi-|=$M#u4&^+1t}oW*kbU z5~u)>L}ou;x;~Hs{SQRYvC18*8||R0V|LS64B_A{*zqF*71|}6Zg3F&M!H3Wgd~@u zES3#=xTk~m*;wkMbhbM{V;m06x#_!H&N(wVQMd}%$l-W9Xc&tN3ucgiQDc#z+(eFm zq8xi7B?X(>dK=PrCu|IiQs6A7k20{iy4v@y-{u~}H)zGFdQqiWfgLqaaG51ZBjFc? z^@Lp>@=C92q9#iv?0?Q}zXtJLraJk)mRh!<-~`3%Y>9hKT=PEXswzZ$QX~-^+<=GhX)8j1||zU_6`oF+|v;DQ3hJ|U}NL_c^eEtrw|32+vn4&%gf8n zR=0}9)3+0?uEY_|JP24I-|#v=-I!}+KQi4EE>M?g4<}W~gbBcjk_`@Pi6hx&NT{-- zZhVX=fp0E9-Y@aQv)xrrEDbnlmwwdr;db3K5%@oDDJD_z^7HQoFr7b+EK z#D8QJ5Zc`J^FMZsm{H_9`T8QSXvpmX3BXFh>ZDlC49YU(x6I1f*%^o*D5tl#$zy&= zZbq=|LYq>(WG&b??efI#;&qRf1{(k32ZWTjH$Pjfn@{(4uqqD857UO(Mp~0Rn>hEC zo3+SoMI>UGacU}oRHU_<%)OsG2BL5FBRy`J{=*@@Ubo8{1y?JCdp4lk;HhXVUN^fdm-C z%l)t}4L1wa5_Ii!i}~ei(*|8TR=nsT)4l9C&L|i?-~G`Pi@K2v59LM|=A;pztl{bU zIl(Jkms~rs&?t7N&;0N4+VqWq$S$Z#pE+&B3|)I985Az$R1tY-U6C-Sc^`8W#)qqo z^rQP5oz70Q`63IwiDOl2f+cHbC-an(ii7IpKRPI84NJ7XcypZ^2M+{4znQ0Dc;`85 z=iNb+R9mvic^i!yr)4GA_yyqDNKE|l1*8@M@axZ6%02i%f2l$sLJqM-lutM1o@|SA ztV2X-h6W8oY{1v4);ogfkSMXU43`7$WL_iG8B0x*dTWR78B^_^|9B%<#PT0P>AqX> z6?U*+pPq{4rW9F>Aiy+59tHoi1b=zpJzq5Z@pdTu!7Dh$&(E)`juJ6)p)*uQ6-^?w z;{QE6GgGZts{|A6H%IaAP5sTFCk+K7XAX=Yk%?5F!%gcZ;@-zcE|@{FVN)&yyoXFB zwNYNfW5}av7Cf)EFG?ko7~Hj8LHE-A`#8YtQ9Gmuc5|FD^lPbF5#AO<2E{S^PtEe| zbZ++DBRGp0Q1dbkB6hqBMk>dok(}lDWOPGKZYxq_c--%jR)^a?y2D+X;;mht5{W|v z=`QEsfZVFQvJ8r>JAX?`_^p`MRsL*L>!pBP&48V`v&k+-Xw670AhrcO-8eWOOcTV) z!DWxM7c7Uj44O-*+#StOPUC9gPLY#2(0#}LCgA}lFKCdQ#Cyhn(B^7)?47I{QxQ#Z zw=*C@?o%Exc9pCFy>SimIBX@|YN(l@FLa3T@M1;^|NI6kM37I>OCE=ny4^wIJa~o^ zC7E#+QI8OjUG-)3Ph?x)_SMtv;WPOfJFeDm&OSJKQKqP5`Q|cGjz1=YknKhgOuhl6 z=c1dnwNG3<-=q5ULvZSVD!bp~S7>LQ6BtZFbnKg{em-vV+ zVa8)Zs&SOTc=q;D5g1!Ug@mc_*n{0*J4%z1wdi+t?8*n|FRC}R*Xz(gg?BQB4zpA2 zN~aRnE(Q?QrE|f$DRSmO76#-Apco%8xdvWy{T`OPr(mmbXda?^*@{BMwt;AIczCFk zBLL7ZERXw|)x2CSCPajCM%HHEiXHP}rvphdzVvBU#0wt>tA5~Cejt6udgi03cPA-3 zHqL9n{FQ|u>NLht7X;OXxVv0usXLGLh@lf;$bH;??6QG~GG0&L4m%I8f1PFsPJfi- zzuE5p3h`l5Q%1$0HQ00Yd7%VJG#(0|%b~(kwF?+8?Huw!{Lm+sou|n08t%jNF)kPT z-84_k1PhLG{X)Ma_~&f8wI10fw+$dmHhphVwlU3r18iKpW$Hi86piy(v*shA9Vxqv zD&Sap-yI!HWJVTi;Gp_}kAcS*F;k^PRHG_wX5NpLQ8BXp*f)G17!}e<#}BC<$i=rq z^~01bSp|ea3X-q94hkdPJcm|k!KQbL2#h=N0YNa3HM#mN?d^=e0HSj!j@0D~is|fn zKlnJ~wmdf95Tb&Uv{oDr3u%&%-e-!}`O6}ElzgFwhZ-_V!pPHYd z=s*as)>for-6Dfq3Dv4`_-(|b(zW^H@hZPp7eoC#Fu~BCGy`_?WH`zXE&O)mGx!Zy zMMLltbzJ0D9~>2TtSPnJQv^>eNBTHW!}0epluGU-lr6|t>V2u3cq$>jl`g(B_l zU0)(H((l6ayZXT*EW7>yz&!vYs4+8k#z-~9G?t2fg2z;Cb{Rmn0FDv>^o3uRr<$!_ zgFDM$6#^U>Xz%uElKD{lPT<}l!>|aY!1-asHv0mO6xqkV8tf0t;$rB^N#oh0HDz0eS-UhzPYne=`R5$&J+7ok64MD<aBT!;TOA3US8^Lebwx5>ND4)k2%kvEk|JkD04NF{s&UAH z{v+D08LWXCdHqdgdH=yrMR=I31)~`@)0If->d{KM4NK*(D+R?IX75XblBc355q{Aq~-tg1YQ=q~v!NY~=In4b*^4L;}oA53P z&k6lx*H1Q5cg%>CG+Px56SK?Xd0N?oUQk)dRH^fLR2a9_$drBhg6Nm+!Z<^pA_pW- zIb6LZBt)?*fNES>A+RPTkGF4HwdIOf&r7SOl#pKHRb!;XiF!0UByyu7v@;^dK%KXw zT9M;}491F|LjN9ml#ng}W+STG(pX{b`{`h=6dAxf*#by5HVrlvB{5RszHkWP&jQ#; z@A%=x8f@L}FG=X?w`eMM`cCEQD{E`&>)H`hCN(n2+;@da4-XGH0$%KR$T`A4NF--A zHa38*9Hak@BSAQ_pq`Z5FX6cnf-E_LyRo~6&%d7=3hC=fjNdl#zHJt_#0AtN#@fzk zIiNzEpWB(_YF5l!amp09wSD7zmh?W%!ZDAB6@w%H2WTlsoy(fbfAHJS+WQm|B~0&dNw@3`Vqjg_!}r zpcW;Mu`}^6Wz^=0spS&ok{$V6lt^@UWHd_3_U^7J4x%!FL{$)3b^g=N4h0g*_NR1p zSbMG*KzA^^Ig-;dLJkL(G%PI4T9|s2SNb!YEIcFoh5EgB!^v1(f@MmdKS<&)a&OWO z+AA0Bqz?~6ohnOad;R!yhI5T*6rQW{GJ%TDzP6<=53OMV996uyxhE&VjHo#zkI8U4 zzytT6?uj0&0}|3-XSaWt3iGJ6BTyjR^*H6B9y-MGh207bl)X8|M3Moo#TE8FE6TdT(b@y#JQ#La>&pXykW4Ch&Z z32M03NlM#F`g3QreBMi)GAJu6tJ+3Ei|o6EuRjZQp;1%IObq?Nx$iiUBnHA;0cl;~ zg(JC2zci1WU%@2rRqi)MtV|sD>!8~&Yf+ix-3y~gmfdI{k)QP?awprjIWsGx{6-9( zqVbj93uwlvomdKw`XWD~GB$>Q zVY2XKKNHvJuQ#n5qR-%JF5Cei?s%DSKaHTg4ENE3T(OditKmzyo#=ruQcWR|FWw^u z{X+blg)jk^&(65c(j4n@7aB3Lyqlf=nM{woVSsj^N^v;%JU$y#?D=>4Zv2SSvXw~v z1E~+4t`RR!8Pu&Z{03l7+2TYe%nyJ`rZ(5XNgpwDJhE78gBmqQPfk0KU#*)~16T;s6x(*VRAH6&85z~nHgqUA;;ie5~w+&5ys0*863GXe5?aaHaC07h+plF(z32eu&`d< zza$V&Q^bFMY{{hVBK}!#3}@j#{`U_+neFhrpVduaQQHjQYpI2C!8hnLtNjr@A@`m^ zJCcU4MgvS>>Zw^S0HKYVR-5}63R_o{Ovb zwt@?1g_4sz2V{{RqrCfN5kA4eX{C~dvaD(DVzKX;Vm0}~#9IFaNWcB?EJGtEX@WQ!QM#sS|D722>XU1mk^m6k#z zC_ZV0J|o9((VCSPfC62Lxc1en>1$bE89)k1!ppRdrP?`E2tjS=EL>85rEzd!Ev#dF z6Pqbkmb)|`m{c=n5Kb{YZ+kz1epzJDNEG2mg+WT2hS#%-I>v`bL+NaJcqdqt-6Osm z7naOfx9GnE9I)Tv8Xy$*^aP|P@_z0QI1R{@O;jjVqwY9Y!zixcUVp+SMGUk}NF}go z(0#j(`_bOn8M}h*tM{mI#L#AT3u;dz(Pl8~h`QfquUw#R{|mv~qreY`C#JI?9X>j# zho*OdA#ZAkM3E3vmh9Sg=?wK?q$o}AGC5i)IblJUKc`T<>cG5*vHIa7Zj`d7JA-Vj&ImZZ0wpHCJO z@mgVKckAN9zHQwiB~I>}%)<1gXTL){9}NqWYG?^>X7scbXRG_UZlz{sX6Beh9rBf) zfq{i@=7p!dJ@dm{Y_?18k9XdB=mS8Edp_ugW0!=TD=CYXoVhGnIK7uDOb9h)=S-$c zz4r#(d>?bdZ*wn~T^|7T#lQPU!s@W{VcHgo@Mi-zE}c*zQDX13rGjDE3c!$j=z8|8 zeRVdY_X9T0rWcUxixRJh@c3?4CwUH8cOt6+(GsewfyGFQW96vb)IN25#pkjkjcVlK zq0!c3`Rvr>a^p#a4?gg4D)%$a7X4D0=uX6%ib03lsb;07sckWF7SH`EPoxx(K9G}Y zI~O~(Tr7+RmvknOd?W0vq)dEIN9gP+II(j9jy#cre0xAz#*b)7LQ1@=_bc6ZCPudn zcI9?}3~SImf7$#HA{}swhDBd6$+&xc^-ZLD19w?9j;XNgRMR)JnhZZ>h#nP&%F!bAt$e^*AD;r)3c>|6Y~o89`#Dl7=J-=1?&RZkpi)|BI3}T*D%rh z=BdB47Dh)Ai3yE3zC$n$gq5_&bpItG%>dKJ?jjiE<@K9!Vu)YUKlCZHvM7sIv_^sp z7u4xB7;!|uz)1fmJORGbFP=XaeAtPKo*sE&H^^L5rRy{jTN9h{+d!jUrNw^^rmN=q za<0a8S2;-HUf#g@${?I51q6@I5A5wZBJ25=VezZ9hdF=`CFHi3p<_y++U3fLFXG>X zxiqN!-l-r7BP#NVc~I0i*(m7G>ai<);<16#B%7u`Woc>YqlRV{WEtM&c+%A#kOC&B z;^xBbzOpc;$o*&CF>7?`?P$dk8O}h~AR3}dw#^$likcGs$f53+Tf#B)($6QOKHpj>Mp$RwYNrr&Bq&nBM&wZXZVqM%DjYI{1|@CM-D?2cd2`~jGM zM_R`j<`Hp(VI9CQ4UnDVWQlCqiq{|G2^MwXfXYmS+JGoCMpFR#wRmc^cW`h3xO+(7 zw@Lq0R?hrnDpF{)_`(|g9&lhEEURAaq(0S#u z2Jsp%BH;IoRj&FE#f{BYt8fOQ#FWs{4e-FELHE(1fu#SUuljA_qrq}rV-tkOBL zbDr4zUNiQdgwW2l(Zn#h;@XA=4d%4vfI!*3@zbzCwJv`6dCnNBCILfzpoVOLF>L#3 zMTyI`luGN^iF}M);Ar6^2nzY8nP>x8LqcZ~Ho5;S+n#t_fBcl#D1DPcFG$mr#CnZ3 zYm_JVGT(*XE7WIpu(e%laVdR0p<-3(*8Rbq6zN~=nQzO5)Z3E+D|$6To4eNP_Wk>J zhRlhA%qSodIku7rtk$3vS18GcwDDm0X?Fj-{&QeQ9+GHV*S#Hx{hL4{>0=C5U4jk9ONaEu59LjElV(R418kQ51Ol8N=oWcRhl=tz;@OG*4(-Q96uAU}E| z0OjOi284x(7#?0D{I~^ukr6+#;ok@ zYl$UeRraDCyJ~l5XR$UYytGd3Zut%a+Izysl#~V}Wx^uDN`h^tz)!il4Di|HeF>MTkUmR88L|K~8)SPk zC2zA~lmF@j9Mlf$t!^(5c5N5>r)=F+g z)^CnLnaKtp`wHqHK&(SD_Cp70uv_GwiRE7{Tjm0qi~Pd~?6zANcsXPNQp68wBNV($ z?E!L9!HeF9LY(-O<6X91pm`UK-AW!jDYi6C=j*(oiW}HAz*GvxNZ{}{(X6Iff^>B zuK3lJY&z!EzVF}VX6~_UiVDqYPo;_7Bd(yxYw28h1D+WR74tAPB^2ipOil#%#Z)HhKXZDznZkF0q?*9AFbwd6NU^7uBYtB|? zz*T|@c6O#9`YQD49HOha~IEQo@1?mBkOi2jULV1^}R=r87>`wndMcaR;AN7+;{%LuTmqjY9D{lw z^ora$y?G3-At!mcI>!!g<0FR9qXWXDNxGxNc=pmBVRtzm=4v3l{1q$2Md(E5XRLy) z6T^TWiJ~$Fl$3iS)UegnRZ{Syao6U@)6@?kf|$NPbs{dWPzw*TH58<+Yp4gU?`8YWMW_4C96*7FoaelinrE*`&Vn64`-S>548Ova(K*UIn$9oc#2%iie@HDUskIF|1guv~s zvJ&P!Tm!s^DE)WdH3@`+SgotsE$A+s^6#IA894%7U8pP@mFh~Q^t2Kj=i><_E!{(TY_c-lPO4tHPrQfT z9VDy7^o;T13so*Ob^LZFa0tpJpa-S^7DRSLnqnAE!L5wycZ!$-rVu0Et})7{SXW96 zzK;T?(H9hrTd;>%G6YE-q<>i#5TIIG(jPMGYlzwCtB5ch$&yAm31a@=>;TZPgsL@4 z3_luL7ijQGz}i7@w;;iwPeMXMgJ~zF_w9LlJdsFPQn?B=Az}d|(10a#y-jyB1JQ5F zWX7StsGS$xpr$S3K*GFCRz3Rjf{y~$9<*v?2TU;}=jS^%U?7~_J zzo5Xj6@sAJ5tA3n7)T~SP;~y)3$6^=Zw3w@KXaFApz=z4Mp&B1(AKnVzFa2-?!F=X z_vU4k-dg4v4=q5iqBq@2{Ay(Quoq+{R_4mh=RqO^jLuHOleFvg*=dpU+Elyx^-a>J^XXkxG! z=sqBpj7nXX%-a|{7+2qiK%kE+3|_hb-BPMGmx%NI$eB|!Lck19d&tUA!zaGB z<&}Qe^bC+jJ&7^)=;@$HE^clNso}5n;V6FvVBKTLXan97;I_`UduC=5_z3q?6SyEV z%MEq@bgSVDN)Rf-BNlKvlY5K5D=KQEE!i~un*a6uo>jG28r<*+OvrdhW6Rd-q?#uYN^wFyfFxox)3lx0{eCnI_>_2fG*XXqGWe8e3X`%;n57(%tlE2%8nY?pamQgB>A& zo0dp^%r0&8Rh9waiWQkZdvc&C^R@zC#?AQ6E-i?$Qz7hGc?mB5)(PU5Nx{EaIF;1s zkQ1&%S4rOm8l=Z^czP7T^cx~Z{>9e~s<0P?k2BJlf$*CWHc^c7$e>aa!6`(o^05NO z!LQ*fsnfK{gg*1oY?b5}} zkY`gBme~1rXPi$tDeIMCRPtpe9YrA|JD<#dO7nAyME^}$XLEbI!VoqoIeY)qFF~Du z-O>xnH&m}TnE0fLkgL8`Y z#mCpFMxR?af@NSE!u!7LC+}VC&XKMFcyJ`qqUb4^jVPURcnjo@OGBp3Z2q>J+o9@X z=!Qthp;gc^WlkiTZ*3-SO3(48scuQ*6n>4uP%L@0 zaz63GHr;>_b1bW&*WIz6w>hK9Q2}4#UD>sFLZouofFn=4({AV$8aCdx%D(R&oSBF- z*aJ$>q+uNsXJ*!$>HuXfFWXE?89y?sd+84+Rj&vj-kCL)ZgDSnx zRElM6@g0KM_{}4R7x9FehC+AP?jkw~TD?AFtVA|vxT;rVbiDrkES(9&xRGHf!I-lU z^G~6D`5{GX9e|@vmQI0O7@HXjv=sSc>yx^6c~hmR(TQq#e>YY%?M18aC5(`muixQE7*lems<<)+^ zN=YidMPBp4rqJ|@{0vc<+wZevu%n~M3TC0GX=+T?GHQ zPc{*tS!iW3C1-9Ar{<3D0GB6IT|p-m!7=hHqG0hd0&nG(k{_vhgYLxRc`sr}uRJOG z^Ap?6@j@UH9lLlhuQZY{JnrYc*M8Y)SkjsPcq#;erS?;ZzAMhCVB%AQBPs>OVvr%_)ar4=~Q z)SzSzbXTmsi6>@lOBy4C#5ZfM%mSWTmLSUHUJUp*pr!ztEhi=>{Zt0X9HN zVRx%^0GJ#-@l^_P_bpgEjilxae9}f;6T*!S@ozK6NL1d2o5%4!D3|bKJ+BsQ< zO!*M;({iG@B-@f?aD+Xyle{65k?pgcXo>GrP1Mw}rhpsbj;$@mDaEK{eqMU0CC zZ1T06UO=lI09bc_Y7X&{9tTE4j==t(8Smwsh{N1Sa9=;M``}F-GLrowBypVVL)! z@8`-vNaMR2ih>3k{yh`XL@G6ms5Dmf_4r@5GI3<7Q22mQhh_vJl@%so2Y3h%U)n(*t@2u?pEK9vQh^O2Y0eAKx#FrqhQhm4JJmR>fp!mRFvuA%)Z=2UrtlJQ zrDTe+BH%F{c@R?7g)5{>2UqmWEjW$@1bDwzY1rG}u znHlggO>wBlODA6rS40o0AvDUxomXSo2x(6Q??AaB2cN#z46C4?XT{R7ICvu^&fT&}k;)2;~O zYCUTPh6DEM_5?5A0hn#Ep1fr26q-WKKdC=f6Uwl<-gkt^CYCGFA%99z-VzF)@$;8Ch7WDl0{W+{+Fv2EhS=8JM;N%$K+`eVAg31|S?P_;J-# zGI;F=TbCbiU_&If278^Jq!XgZ;FO#sgxK`l}+Hd zjo@f_Qc}{-(RqG0v$cjpHs!JsxIOdo+rerHRy#tS{Mm}j(_!Av{m}x$>8Kdy(SX4} z0-#xjPELN|+^8izS8hHtE&SK(P5SXTRH=!CHe<`>(_v7oiZH>roNn9c5-aiz15?Dv z*q9t1DieX04ut|QGlGJ}eO7HVb##Yvb2P7)5ByUI+I(7ORu1iGNbNk(9k2y>B|?r) zEEfcJ+ixsxrr2rFtB^y@+6ASsB5PPHWAZ<=JvkBM+}C+No>p}k=;*XK?<1DS8h$)4 z{xo!l<>TRje>1BqH$?4dZ;y^MgY@p&)B;cN4$bnOsO<15B|o|aHHwAJN4}9IB;W2o z{?5(sX^7YNWwemEX1yknu9s12Aa{FX5RRSG3=u)rR8$v?q{pi8=gwAyRnj>5^$Zp! zR9W3Pn7%B{ny?s|wYs3oCcV4=Y^N(irT6-X*v5Ai?X?L{yQM{I8Ry*<4*h!|m+cQ$T9=bJs8V#wdu)gUxHtaSAFN zM;G=k#lK%kSO3PfQN#>p7k2)1%_`WVyz%McJA5ZK@}uW>xpUwJ{tpF&tfZpEA2FlA F{{cuUG`Ii& diff --git a/Tests/images/palette_sepia.png b/Tests/images/palette_sepia.png index f3fc932531f9b73c8d214e7c236c556ab3775d22..9e7d6b0345b2c9193b97d61c97a9dd4bdee668df 100644 GIT binary patch literal 20364 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 M07*qoM6N<$f@Pis-~a#s literal 20339 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 diff --git a/Tests/images/palette_wedge.png b/Tests/images/palette_wedge.png index 23fb7940d6d61c42e69ad3c80fca5e77c26994d3..4b3d9ff3a219dea6f7ee2fb9fdc9382c341804bb 100644 GIT binary patch literal 16984 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 literal 17065 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 mp1g?hKH_PmYNYWMwhbk94palette[i * 4 + 0]; - tmin = (r < r0) ? RDIST(r, r1) : (r > r1) ? RDIST(r, r0) : 0; + tmin = (r < r0) ? RDIST(r, r0) : (r > r1) ? RDIST(r, r1) : 0; tmax = (r <= rc) ? RDIST(r, r1) : RDIST(r, r0); g = palette->palette[i * 4 + 1]; - tmin += (g < g0) ? GDIST(g, g1) : (g > g1) ? GDIST(g, g0) : 0; + tmin += (g < g0) ? GDIST(g, g0) : (g > g1) ? GDIST(g, g1) : 0; tmax += (g <= gc) ? GDIST(g, g1) : GDIST(g, g0); b = palette->palette[i * 4 + 2]; - tmin += (b < b0) ? BDIST(b, b1) : (b > b1) ? BDIST(b, b0) : 0; + tmin += (b < b0) ? BDIST(b, b0) : (b > b1) ? BDIST(b, b1) : 0; tmax += (b <= bc) ? BDIST(b, b1) : BDIST(b, b0); dmin[i] = tmin; From 128ed189e58e2679ad5cdbc22937a3109282e340 Mon Sep 17 00:00:00 2001 From: Ray Gardner Date: Sat, 18 Jun 2022 18:07:58 -0600 Subject: [PATCH 014/242] Improve test in _get_optimize() Palette can be optimized if number of colors can be reduced by half or more. --- Tests/test_file_gif.py | 15 +++++++++++++++ src/PIL/GifImagePlugin.py | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index c261cfb97bb..2bfaef4ee77 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -180,6 +180,21 @@ def test_optimize_full_l(): assert im.mode == "L" +def test_optimize_if_palette_can_be_reduced_by_half(): + with Image.open("Tests/images/test.colors.gif") as im: + # Reduce because original is too big for _get_optimize() + im = im.resize((591, 443)) + imrgb = im.convert("RGB") + out = BytesIO() + imrgb.save(out, "GIF", optimize=False) + with Image.open(out) as reloaded: + assert len(reloaded.palette.palette) // 3 == 256 + out = BytesIO() + imrgb.save(out, "GIF", optimize=True) + with Image.open(out) as reloaded: + assert len(reloaded.palette.palette) // 3 == 8 + + def test_roundtrip(tmp_path): out = str(tmp_path / "temp.gif") im = hopper() diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 3469199cad3..f58146c7ed3 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -824,9 +824,14 @@ def _get_optimize(im, info): if count: used_palette_colors.append(i) + num_palette_colors = len(im.palette.palette) // 4 if im.palette.mode == 'RGBA' else len(im.palette.palette) // 3 + # Round up to power of 2 but at least 4 + num_palette_colors = max(4, 1 << (num_palette_colors - 1).bit_length()) if optimise or ( + len(used_palette_colors) <= 128 - and max(used_palette_colors) > len(used_palette_colors) + and max(used_palette_colors) >= len(used_palette_colors) + or len(used_palette_colors) <= num_palette_colors // 2 ): return used_palette_colors From f656711c80d039e60f86923b05dff4866815f2e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 19 Jun 2022 00:20:25 +0000 Subject: [PATCH 015/242] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/GifImagePlugin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f58146c7ed3..b30ed1728e3 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -824,11 +824,14 @@ def _get_optimize(im, info): if count: used_palette_colors.append(i) - num_palette_colors = len(im.palette.palette) // 4 if im.palette.mode == 'RGBA' else len(im.palette.palette) // 3 + num_palette_colors = ( + len(im.palette.palette) // 4 + if im.palette.mode == "RGBA" + else len(im.palette.palette) // 3 + ) # Round up to power of 2 but at least 4 num_palette_colors = max(4, 1 << (num_palette_colors - 1).bit_length()) if optimise or ( - len(used_palette_colors) <= 128 and max(used_palette_colors) >= len(used_palette_colors) or len(used_palette_colors) <= num_palette_colors // 2 From 5f6bc0e1a6598e375542de80d59cfd69a9c21fa9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Jun 2022 14:07:50 +1000 Subject: [PATCH 016/242] Removed #6377 from release notes --- docs/releasenotes/9.2.0.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 83ba28df85b..424fd487a29 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -76,9 +76,7 @@ TODO Other Changes ============= -Fixed bug with rounding pixels to palette -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +TODO +^^^^ -Fixed a bug when rounding image pixels to palette colors. This affects the results of -``Image.quantize`` and of ``Image.convert`` when converting from mode "RGB" to "P", -"L", or "1", regardless of whether dithering is enabled. +TODO From 709744432a2a290170d2b01e4c39fe2789f505f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Jun 2022 16:47:50 +1000 Subject: [PATCH 017/242] Optimise palettes with more than 128 colors --- Tests/test_file_gif.py | 32 ++++++++++++++------------------ src/PIL/GifImagePlugin.py | 15 ++++++--------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 2bfaef4ee77..dbbd3bf9de9 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -158,6 +158,9 @@ def check(colors, size, expected_palette_length): assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) # These do optimize the palette + check(256, 511, 256) + check(255, 511, 255) + check(129, 511, 129) check(128, 511, 128) check(64, 511, 64) check(4, 511, 4) @@ -167,11 +170,6 @@ def check(colors, size, expected_palette_length): check(64, 513, 256) check(4, 513, 256) - # Other limits that don't optimize the palette - check(129, 511, 256) - check(255, 511, 256) - check(256, 511, 256) - def test_optimize_full_l(): im = Image.frombytes("L", (16, 16), bytes(range(256))) @@ -182,17 +180,15 @@ def test_optimize_full_l(): def test_optimize_if_palette_can_be_reduced_by_half(): with Image.open("Tests/images/test.colors.gif") as im: - # Reduce because original is too big for _get_optimize() + # Reduce dimensions because original is too big for _get_optimize() im = im.resize((591, 443)) - imrgb = im.convert("RGB") - out = BytesIO() - imrgb.save(out, "GIF", optimize=False) - with Image.open(out) as reloaded: - assert len(reloaded.palette.palette) // 3 == 256 + im_rgb = im.convert("RGB") + + for (optimize, colors) in ((False, 256), (True, 8)): out = BytesIO() - imrgb.save(out, "GIF", optimize=True) + im_rgb.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: - assert len(reloaded.palette.palette) // 3 == 8 + assert len(reloaded.palette.palette) // 3 == colors def test_roundtrip(tmp_path): @@ -997,8 +993,8 @@ def im_generator(ims): def test_transparent_optimize(tmp_path): # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # transparency. - # Need a palette that isn't using the 0 color, and one that's > 128 items where the - # transparent color is actually the top palette entry to trigger the bug. + # Need a palette that isn't using the 0 color, + # where the transparent color is actually the top palette entry to trigger the bug. data = bytes(range(1, 254)) palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) @@ -1008,10 +1004,10 @@ def test_transparent_optimize(tmp_path): im.putpalette(palette) out = str(tmp_path / "temp.gif") - im.save(out, transparency=253) - with Image.open(out) as reloaded: + im.save(out, transparency=im.getpixel((252, 0))) - assert reloaded.info["transparency"] == 253 + with Image.open(out) as reloaded: + assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) def test_rgb_transparency(tmp_path): diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b30ed1728e3..dd659c95905 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -824,18 +824,15 @@ def _get_optimize(im, info): if count: used_palette_colors.append(i) - num_palette_colors = ( - len(im.palette.palette) // 4 - if im.palette.mode == "RGBA" - else len(im.palette.palette) // 3 + if optimise or max(used_palette_colors) >= len(used_palette_colors): + return used_palette_colors + + num_palette_colors = len(im.palette.palette) // Image.getmodebands( + im.palette.mode ) # Round up to power of 2 but at least 4 num_palette_colors = max(4, 1 << (num_palette_colors - 1).bit_length()) - if optimise or ( - len(used_palette_colors) <= 128 - and max(used_palette_colors) >= len(used_palette_colors) - or len(used_palette_colors) <= num_palette_colors // 2 - ): + if len(used_palette_colors) <= num_palette_colors // 2: return used_palette_colors From 607f5080808d38dbc5e1d89c18ffec5d77f6f4b9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 19 Jun 2022 12:29:39 +0300 Subject: [PATCH 018/242] Update comment We get "an integer is required (got type NoneType)" with Python 3.7-3.9 on Windows. We get "'NoneType' object cannot be interpreted as an integer" with Python 3.10 on Windows and all versions on macOS and Ubuntu. --- Tests/test_imagecms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 6dd38894177..0ff8fc7d225 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -189,8 +189,8 @@ def test_exceptions(): ImageCms.getProfileName(None) skip_missing() - # macOS/Ubuntu: "'NoneType' object cannot be interpreted as an integer" - # Windows: "an integer is required (got type NoneType)" + # Windows, Python <= 3.9: "an integer is required (got type NoneType)" + # Others: "'NoneType' object cannot be interpreted as an integer" with pytest.raises(ImageCms.PyCMSError, match="integer"): ImageCms.isIntentSupported(SRGB, None, None) From 1229110fb2f9acd7a0bf7eb21c256b534211a2b6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 19 Jun 2022 15:22:02 +0300 Subject: [PATCH 019/242] Update comment Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_imagecms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 0ff8fc7d225..3d8dbe6bbf5 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -189,8 +189,8 @@ def test_exceptions(): ImageCms.getProfileName(None) skip_missing() - # Windows, Python <= 3.9: "an integer is required (got type NoneType)" - # Others: "'NoneType' object cannot be interpreted as an integer" + # Python <= 3.9: "an integer is required (got type NoneType)" + # Python > 3.9: "'NoneType' object cannot be interpreted as an integer" with pytest.raises(ImageCms.PyCMSError, match="integer"): ImageCms.isIntentSupported(SRGB, None, None) From 406fe59242ad288bcd9f9fe663b227620eacd344 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 25 May 2022 01:57:13 +0100 Subject: [PATCH 020/242] deprecate font.getsize and related functions --- src/PIL/ImageDraw.py | 66 +++++++++++++++++++++++++++++++++++--------- src/PIL/ImageFont.py | 27 +++++++++++++++--- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 3824626bd6a..4e5ee24b71c 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,6 +34,7 @@ import numbers from . import Image, ImageColor +from ._deprecate import deprecate """ A simple 2D drawing interface for PIL images. @@ -372,6 +373,18 @@ def _multiline_split(self, text): return text.split(split_character) + def _multiline_spacing(self, font, spacing, stroke_width): + # this can be replaced with self.textbbox(...)[3] when textsize is removed + return ( + self.textsize( + "A", + font=font, + stroke_width=stroke_width, + __internal__=True, + )[1] + + spacing + ) + def text( self, xy, @@ -511,9 +524,7 @@ def multiline_text( widths = [] max_width = 0 lines = self._multiline_split(text) - line_spacing = ( - self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing - ) + line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: line_width = self.textlength( line, font, direction=direction, features=features, language=language @@ -571,16 +582,33 @@ def textsize( features=None, language=None, stroke_width=0, + __internal__=False, ): """Get the size of a given string, in pixels.""" + if not __internal__: + deprecate("textsize", 10, "textbbox or textlength") if self._multiline_check(text): return self.multiline_textsize( - text, font, spacing, direction, features, language, stroke_width + text, + font, + spacing, + direction, + features, + language, + stroke_width, + __internal__=True, ) if font is None: font = self.getfont() - return font.getsize(text, direction, features, language, stroke_width) + return font.getsize( + text, + direction, + features, + language, + stroke_width, + __internal__=True, + ) def multiline_textsize( self, @@ -591,15 +619,23 @@ def multiline_textsize( features=None, language=None, stroke_width=0, + __internal__=False, ): + if not __internal__: + deprecate("multiline_textsize", 10, "multiline_textbbox") max_width = 0 lines = self._multiline_split(text) - line_spacing = ( - self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing - ) + line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: line_width, line_height = self.textsize( - line, font, spacing, direction, features, language, stroke_width + line, + font, + spacing, + direction, + features, + language, + stroke_width, + __internal__=True, ) max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing @@ -625,8 +661,14 @@ def textlength( try: return font.getlength(text, mode, direction, features, language) except AttributeError: + deprecate("textlength support for fonts without getlength", 10) size = self.textsize( - text, font, direction=direction, features=features, language=language + text, + font, + direction=direction, + features=features, + language=language, + __internal__=True, ) if direction == "ttb": return size[1] @@ -704,9 +746,7 @@ def multiline_textbbox( widths = [] max_width = 0 lines = self._multiline_split(text) - line_spacing = ( - self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing - ) + line_spacing = self._multiline_spacing(font, spacing, stroke_width) for line in lines: line_width = self.textlength( line, diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 681b75d448b..38fc24d7481 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -143,6 +143,8 @@ def getsize(self, text, *args, **kwargs): :return: (width, height) """ + if not kwargs.get("__internal__"): + deprecate("getsize", 10, "getbbox or getlength") return self.font.getsize(text) def getmask(self, text, mode="", *args, **kwargs): @@ -386,7 +388,13 @@ def getbbox( return left, top, left + width, top + height def getsize( - self, text, direction=None, features=None, language=None, stroke_width=0 + self, + text, + direction=None, + features=None, + language=None, + stroke_width=0, + __internal__=False, ): """ Returns width and height (in pixels) of given text if rendered in font with @@ -438,6 +446,8 @@ def getsize( :return: (width, height) """ + if not __internal__: + deprecate("getsize", 10, "getbbox or getlength") # vertical offset is added for historical reasons # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 size, offset = self.font.getsize(text, "L", direction, features, language) @@ -495,12 +505,15 @@ def getsize_multiline( :return: (width, height) """ + deprecate("getsize_multiline", 10) max_width = 0 lines = self._multiline_split(text) - line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing + line_spacing = ( + self.getsize("A", stroke_width=stroke_width, __internal__=True)[1] + spacing + ) for line in lines: line_width, line_height = self.getsize( - line, direction, features, language, stroke_width + line, direction, features, language, stroke_width, __internal__=True ) max_width = max(max_width, line_width) @@ -516,6 +529,7 @@ def getoffset(self, text): :return: A tuple of the x and y offset """ + deprecate("getoffset", 10, "getbbox") return self.font.getsize(text)[1] def getmask( @@ -796,7 +810,12 @@ def __init__(self, font, orientation=None): self.orientation = orientation # any 'transpose' argument, or None def getsize(self, text, *args, **kwargs): - w, h = self.font.getsize(text) + if not kwargs.get("__internal__"): + deprecate("getsize", 10, "getbbox or getlength") + try: + w, h = self.font.getsize(text, __internal__=True) + except TypeError: + w, h = self.font.getsize(text) if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): return h, w return w, h From c854bf8d1c05022bec4309fbf6b547e494db9373 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 25 May 2022 18:06:20 +0100 Subject: [PATCH 021/242] add getbbox and getlength to basic ImageFont and update related tests --- Tests/test_imagedraw.py | 28 +++++++++++++++++++++++----- Tests/test_imagefont.py | 7 +++++-- src/PIL/ImageDraw.py | 4 ---- src/PIL/ImageFont.py | 27 +++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 69d1ac9fad3..dca7bfe6a58 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1232,9 +1232,10 @@ def test_textsize_empty_string(): # Act # Should not cause 'SystemError: returned NULL without setting an error' - draw.textsize("") - draw.textsize("\n") - draw.textsize("test\n") + draw.textbbox((0, 0), "") + draw.textbbox((0, 0), "\n") + draw.textbbox((0, 0), "test\n") + draw.textlength("") @skip_unless_feature("freetype2") @@ -1245,8 +1246,25 @@ def test_textsize_stroke(): font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) # Act / Assert - assert draw.textsize("A", font, stroke_width=2) == (16, 20) - assert draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) == (52, 44) + assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20) + assert draw.textbbox((2, 2), "A", font, stroke_width=4) == (-2, 2, 18, 22) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 44) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) + + +def test_textsize_deprecation(): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + with pytest.warns(DeprecationWarning) as log: + draw.textsize("Hello") + assert len(log) == 1 + with pytest.warns(DeprecationWarning) as log: + draw.textsize("Hello\nWorld") + assert len(log) == 1 + with pytest.warns(DeprecationWarning) as log: + draw.multiline_textsize("Hello\nWorld") + assert len(log) == 1 @skip_unless_feature("freetype2") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 0c50303f902..fe7d79f82e2 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -720,8 +720,11 @@ def test_textbbox_non_freetypefont(self): im = Image.new("RGB", (200, 200)) d = ImageDraw.Draw(im) default_font = ImageFont.load_default() - with pytest.raises(ValueError): - d.textbbox((0, 0), "test", font=default_font) + with pytest.warns(DeprecationWarning) as log: + width, height = d.textsize("test", font=default_font) + assert len(log) == 1 + assert d.textlength("test", font=default_font) == width + assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) @pytest.mark.parametrize( "anchor, left, top", diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 4e5ee24b71c..723ec17fdc1 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -709,10 +709,6 @@ def textbbox( if font is None: font = self.getfont() - from . import ImageFont - - if not isinstance(font, ImageFont.FreeTypeFont): - raise ValueError("Only supported for TrueType fonts") mode = "RGBA" if embedded_color else self.fontmode bbox = font.getbbox( text, mode, direction, features, language, stroke_width, anchor diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 38fc24d7481..4e2cb9686df 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -167,6 +167,33 @@ def getmask(self, text, mode="", *args, **kwargs): """ return self.font.getmask(text, mode) + def getbbox(self, text, *args, **kwargs): + """ + Returns bounding box (in pixels) of given text. + + .. versionadded:: 9.2.0 + + :param text: Text to render. + :param mode: Used by some graphics drivers to indicate what mode the + driver prefers; if empty, the renderer may return either + mode. Note that the mode is always a string, to simplify + C-level implementations. + + :return: ``(left, top, right, bottom)`` bounding box + """ + width, height = self.font.getsize(text) + return 0, 0, width, height + + def getlength(self, text, *args, **kwargs): + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. versionadded:: 9.2.0 + """ + width, height = self.font.getsize(text) + return width + ## # Wrapper for FreeType fonts. Application code should use the From f34a6460ef0925679b6c810fa363bcabcf56458b Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 25 May 2022 18:23:37 +0100 Subject: [PATCH 022/242] update test_font_pcf to use getbbox --- Tests/test_font_pcf.py | 8 ++++++-- Tests/test_font_pcf_charsets.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 288848f2619..885ef843372 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -68,12 +68,16 @@ def test_textsize(request, tmp_path): tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): - (dx, dy) = font.getsize(chr(i)) + (ox, oy, dx, dy) = font.getbbox(chr(i)) + assert ox == 0 + assert oy == 0 assert dy == 20 assert dx in (0, 10) + assert font.getlength(chr(i)) == dx for i in range(len(message)): msg = message[: i + 1] - assert font.getsize(msg) == (len(msg) * 10, 20) + assert font.getlength(msg) == len(msg) * 10 + assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) def _test_high_characters(request, tmp_path, message): diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index a1036fd28e6..4477ee29d55 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -101,13 +101,17 @@ def _test_textsize(request, tmp_path, encoding): tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) for i in range(255): - (dx, dy) = font.getsize(bytearray([i])) + (ox, oy, dx, dy) = font.getbbox(bytearray([i])) + assert ox == 0 + assert oy == 0 assert dy == 20 assert dx in (0, 10) + assert font.getlength(bytearray([i])) == dx message = charsets[encoding]["message"].encode(encoding) for i in range(len(message)): msg = message[: i + 1] - assert font.getsize(msg) == (len(msg) * 10, 20) + assert font.getlength(msg) == len(msg) * 10 + assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) def test_textsize_iso8859_1(request, tmp_path): From 1bf87556ef9953eeea5751714d87bdcc98b49702 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 25 May 2022 22:00:13 +0100 Subject: [PATCH 023/242] add textbbox and textlength to ImageDraw2 and update tests --- Tests/test_imagedraw.py | 2 +- Tests/test_imagedraw2.py | 13 +++++++++---- src/PIL/ImageDraw2.py | 26 +++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index dca7bfe6a58..23bc756bb14 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1239,7 +1239,7 @@ def test_textsize_empty_string(): @skip_unless_feature("freetype2") -def test_textsize_stroke(): +def test_textbbox_stroke(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 3a70176cee5..e4e8a38cb59 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,5 +1,7 @@ import os.path +import pytest + from PIL import Image, ImageDraw, ImageDraw2 from .helper import ( @@ -205,7 +207,9 @@ def test_textsize(): font = ImageDraw2.Font("white", FONT_PATH) # Act - size = draw.textsize("ImageDraw2", font) + with pytest.warns(DeprecationWarning) as log: + size = draw.textsize("ImageDraw2", font) + assert len(log) == 1 # Assert assert size[1] == 12 @@ -221,9 +225,10 @@ def test_textsize_empty_string(): # Act # Should not cause 'SystemError: returned NULL without setting an error' - draw.textsize("", font) - draw.textsize("\n", font) - draw.textsize("test\n", font) + draw.textbbox((0, 0), "", font) + draw.textbbox((0, 0), "\n", font) + draw.textbbox((0, 0), "test\n", font) + draw.textlength("", font) @skip_unless_feature("freetype2") diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 1f63110fd26..6e6a307bde5 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -25,6 +25,7 @@ from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath +from ._deprecate import deprecate class Pen: @@ -176,4 +177,27 @@ def textsize(self, text, font): .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize` """ - return self.draw.textsize(text, font=font.font) + deprecate("textsize", 10, "textbbox or textlength") + return self.draw.textsize(text, font=font.font, __internal__=True) + + def textbbox(self, xy, text, font): + """ + Returns bounding box (in pixels) of given text. + + :return: ``(left, top, right, bottom)`` bounding box + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` + """ + if self.transform: + xy = ImagePath.Path(xy) + xy.transform(self.transform) + return self.draw.textbbox(xy, text, font=font.font) + + def textlength(self, text, font): + """ + Returns length (in pixels) of given text. + This is the amount by which following text should be offset. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength` + """ + return self.draw.textlength(text, font=font.font) From 5fc3b6e5bbe6396246004b48e32afa1e19be231d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Jun 2022 08:29:37 +1000 Subject: [PATCH 024/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8d5d7001a86..afd66e99ae0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Fixed incorrect operator in ImageCms error #6370 + [LostBenjamin, hugovk, radarhere] + - Limit FPX tile size to avoid extending outside image #6368 [radarhere] From a2eff2aff58e2f10ab29c982d25dad58229e1330 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Jun 2022 09:18:05 +1000 Subject: [PATCH 025/242] Added test --- Tests/test_image_quantize.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index e9afd91189c..981753eb9b7 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -65,6 +65,22 @@ def test_quantize_no_dither(): assert converted.palette.palette == palette.palette.palette +def test_quantize_no_dither2(): + im = Image.new("RGB", (9, 1)) + im.putdata(list((p,) * 3 for p in range(0, 36, 4))) + + palette = Image.new("P", (1, 1)) + data = (0, 0, 0, 32, 32, 32) + palette.putpalette(data) + quantized = im.quantize(dither=Image.Dither.NONE, palette=palette) + + assert tuple(quantized.palette.palette) == data + + px = quantized.load() + for x in range(9): + assert px[x, 0] == (0 if x < 5 else 1) + + def test_quantize_dither_diff(): image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: From e2158344a0b4b4016a39dcf40c7220aa77b60579 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 20 Jun 2022 01:20:56 +0100 Subject: [PATCH 026/242] update test_imagefont to use textbbox --- Tests/images/rectangle_surrounding_text.png | Bin 1520 -> 1558 bytes Tests/test_imagefont.py | 126 ++++++++++++++------ Tests/test_imagefontctl.py | 4 +- 3 files changed, 89 insertions(+), 41 deletions(-) diff --git a/Tests/images/rectangle_surrounding_text.png b/Tests/images/rectangle_surrounding_text.png index 2b75a5e9c7ae1fc828125e2903ea6136d52cd2eb..ca77cea7323afb75186315fcd403b7db2509ef86 100644 GIT binary patch delta 1455 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 Date: Mon, 20 Jun 2022 02:18:16 +0100 Subject: [PATCH 027/242] add getbbox and getlength to TransposedFont with tests --- Tests/test_imagefont.py | 39 +++++++++++++++++++++++++++++++++++---- src/PIL/ImageFont.py | 15 +++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index e004edfaff3..606b9ba0ed4 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -315,16 +315,31 @@ def test_rotated_transposed_font(self): # Original font draw.font = font - box_size_a = draw.textsize(word) + with pytest.warns(DeprecationWarning) as log: + box_size_a = draw.textsize(word) + assert len(log) == 1 + bbox_a = draw.textbbox((10, 10), word) # Rotated font draw.font = transposed_font - box_size_b = draw.textsize(word) + with pytest.warns(DeprecationWarning) as log: + box_size_b = draw.textsize(word) + assert len(log) == 1 + bbox_b = draw.textbbox((20, 20), word) # Check (w,h) of box a is (h,w) of box b assert box_size_a[0] == box_size_b[1] assert box_size_a[1] == box_size_b[0] + # Check bbox b is (20, 20, 20 + h, 20 + w) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] + assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + + # text length is undefined for vertical text + pytest.raises(ValueError, draw.textlength, word) + def test_unrotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_grey) @@ -336,15 +351,31 @@ def test_unrotated_transposed_font(self): # Original font draw.font = font - box_size_a = draw.textsize(word) + with pytest.warns(DeprecationWarning) as log: + box_size_a = draw.textsize(word) + assert len(log) == 1 + bbox_a = draw.textbbox((10, 10), word) + length_a = draw.textlength(word) # Rotated font draw.font = transposed_font - box_size_b = draw.textsize(word) + with pytest.warns(DeprecationWarning) as log: + box_size_b = draw.textsize(word) + assert len(log) == 1 + bbox_b = draw.textbbox((20, 20), word) + length_b = draw.textlength(word) # Check boxes a and b are same size assert box_size_a == box_size_b + # Check bbox b is (20, 20, 20 + w, 20 + h) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] + assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] + + assert length_a == length_b + def test_rotated_transposed_font_get_mask(self): # Arrange text = "mask this" diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 4e2cb9686df..ef452778266 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -853,6 +853,21 @@ def getmask(self, text, mode="", *args, **kwargs): return im.transpose(self.orientation) return im + def getbbox(self, text, *args, **kwargs): + # TransposedFont doesn't support getmask2, move top-left point to (0, 0) + # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont + left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) + width = right - left + height = bottom - top + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): + return 0, 0, height, width + return 0, 0, width, height + + def getlength(self, text, *args, **kwargs): + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): + raise ValueError("text length is undefined for rotated text") + return self.font.getlength(text, *args, **kwargs) + def load(filename): """ From a7baa31de854723ddfe0d08d472196feb0e8acc3 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 20 Jun 2022 02:49:26 +0100 Subject: [PATCH 028/242] use getbbox instead of getsize in fuzzers.py --- Tests/oss-fuzz/fuzzers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 5786764a64d..10a172b4675 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -33,9 +33,9 @@ def fuzz_font(data): # different font objects. return - font.getsize_multiline("ABC\nAaaa") + font.getbbox("ABC") font.getmask("test text") with Image.new(mode="RGBA", size=(200, 200)) as im: draw = ImageDraw.Draw(im) - draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) + draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) draw.text((10, 10), "Test Text", font=font, fill="#000") From 3e8a9b2165ec271ade5096e6c59ad642f9f7fd86 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Jun 2022 21:01:10 +1000 Subject: [PATCH 029/242] Fixed loading L mode RLE8 images --- Tests/images/hopper_rle8_greyscale.bmp | Bin 0 -> 6288 bytes Tests/test_file_bmp.py | 3 +++ src/PIL/BmpImagePlugin.py | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 Tests/images/hopper_rle8_greyscale.bmp diff --git a/Tests/images/hopper_rle8_greyscale.bmp b/Tests/images/hopper_rle8_greyscale.bmp new file mode 100644 index 0000000000000000000000000000000000000000..ead32ff95a46d6fc5672974f96c639e680a69830 GIT binary patch literal 6288 zcmeI0_glza7{)(0A=#BAJCeN%SxLhvlD)EbW<=SWgzPOl6lL%1y_J={3k_tiS1#|5 z?_cnKpX;3KxzD-IkI#9Y`@UVgLaq4|K_=jrjs#|Zl4&}`$};`(Z=_$OZ+`u|{v2F9 zeEieF-*EmH{LcS{|2&aIh71|Vm@y+JCMIOcl!?rlGm|At7EDb|$(l7QW@ct&%a)Do z*|U=)M-Fo4%t@|XxyYS6H+k~p!Q9*&3kwS@EiK8LH!oIJR^-c<4{K{{^5@Twjg1Wj z3KXDV!GaVjREWZb3sa;>5sDTqO0iVC2Y=1O^5Y6cog$QKJ|=dNgCkjA889v5Xrxj`8EiGhxC6 zCQh8lq)C&QJb5xxrc7b#)TvCHHjU}ir!!;541$A$nK^SNAt52mnl+2rvu87B&KyER zLzz2wF7xKiWB&a4ELgCBg$oz5Xwf1TFJ8=&B}-VkbScZ0Eo1re<*Zn-f|V;*vTD^T z!otE>y?Qlk)~sRe+O@1(w~qDe*Rx^61~zWoNO*WSn>KA?^XAQL*|LR*hzKGhBiXuj zE8DhhBPuG2=;&y+Z{N<29Xr^$b0@oY?PB-t-R#-3hrN6EvTxr$_V3@%fddCPc<>;H z4jtm~;lsql#Bk)u5sn@`%CTd|IDY&%Cr+H;6<;oSVUcJh-YuC7b{W`I+vBbs2apT4fZr;4fty{Oaefu_d z?%W|hKAyXG?{e?nJ?`JX&w~dKc=+%kj~+eZ@#DuldGds(PoMJa*)yI$f6j{+FL?R# zB?$=$Bqk>E>eVY=zkbb|H*ZKvN+LNqnUs_iQd3iT`}QsG-o4}f`}d@!rSajz2R?rM z$fr-A`26`ZU%q_d>({S*`}U3R-@o(Y#}9t~{K>Chzkauo-@iYDGRj1mlv!D1s;n|o zHf2{1wl)s;(NUDJRuZZPig-)swU8%S8?3DmS^yLk-nPjnzb+@={YZlee17 zM=jJ+t<+j=)K=}(ULDj?o#d;|>Y}ddrta#Yp6aFE>Z88ur~dNO01eb24b~71mA{5* zxB@gnBNeD1jnZh1(O8YscumkmP10md(Ns;-bj?t(W-3IpG+T2Ns=1n{`C6cbTBOBV zqNQ4<W~gAMn`m1$8=mLbW*2uT4!`t=X72dbWxXdSyyyb*K}R6iqj3<)GgiC9mVUe?&-cB z=%F6zv7YFup6R(>=%o^rs8@QeH%d~nQk1H| wfKdTP4={RwF$Ih%VDJEg2N+DyV1kAgFtmW71`Rc6_yL9=U^qhmH%AEl0}b_zNB{r; literal 0 HcmV?d00001 diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index f214fd6bda1..776b499e089 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -134,6 +134,9 @@ def test_rle8(): with Image.open("Tests/images/hopper_rle8.bmp") as im: assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) + with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + # This test image has been manually hexedited # to have rows with too much data with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 4dc2b93c392..5aacb10da8c 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -321,7 +321,8 @@ def decode(self, buffer): # align to 16-bit word boundary if self.fd.tell() % 2 != 0: self.fd.seek(1, os.SEEK_CUR) - self.set_as_raw(bytes(data), ("P", 0, self.args[-1])) + rawmode = "L" if self.mode == "L" else "P" + self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1])) return -1, 0 From f160e698a1e650a06f2287821be8d95be0fbb0c2 Mon Sep 17 00:00:00 2001 From: Dawid Crivelli Date: Mon, 20 Jun 2022 16:20:08 +0200 Subject: [PATCH 030/242] Handle PCF fonts files with missing characters --- src/PIL/PcfFontFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 0556c2bbc58..686237c12ec 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -241,7 +241,7 @@ def _load_encoding(self): ] if encoding_offset != 0xFFFF: encoding[i] = encoding_offset - except UnicodeDecodeError: + except (UnicodeDecodeError, IndexError): # character is not supported in selected encoding pass From b00b509dd817dfedbc7a0943be5c8aa8272043a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Jun 2022 07:54:44 +1000 Subject: [PATCH 031/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index afd66e99ae0..fee35799c52 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Use gnome-screenshot on Linux if available #6361 + [radarhere, nulano] + +- Fixed loading L mode BMP RLE8 images #6384 + [radarhere] + - Fixed incorrect operator in ImageCms error #6370 [LostBenjamin, hugovk, radarhere] From db741bf25f7ad7ec6662f1a813d0505f7882b336 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Jun 2022 16:55:33 +1000 Subject: [PATCH 032/242] Skip remaining characters after IndexError --- src/PIL/PcfFontFile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 686237c12ec..0b43b27acf4 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -241,8 +241,10 @@ def _load_encoding(self): ] if encoding_offset != 0xFFFF: encoding[i] = encoding_offset - except (UnicodeDecodeError, IndexError): + except UnicodeDecodeError: # character is not supported in selected encoding pass + except IndexError: + break return encoding From 317286d260488e1e60a3d25b4d6c3fce83ba12f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Jun 2022 17:27:49 +1000 Subject: [PATCH 033/242] Pad palette to 768 bytes --- Tests/test_file_pcx.py | 5 +++++ src/PIL/PcxImagePlugin.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 61e33a57bb4..ba6663cd3ba 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -20,6 +20,11 @@ def test_sanity(tmp_path): for mode in ("1", "L", "P", "RGB"): _roundtrip(tmp_path, hopper(mode)) + # Test a palette with less than 256 colors + im = Image.new("P", (1, 1)) + im.putpalette((255, 0, 0)) + _roundtrip(tmp_path, im) + # Test an unsupported mode f = str(tmp_path / "temp.pcx") im = hopper("RGBA") diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index d2e166bdd77..841c18a2200 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -198,7 +198,9 @@ def _save(im, fp, filename): if im.mode == "P": # colour palette fp.write(o8(12)) - fp.write(im.im.getpalette("RGB", "RGB")) # 768 bytes + palette = im.im.getpalette("RGB", "RGB") + palette += b"\x00" * (768 - len(palette)) + fp.write(palette) # 768 bytes elif im.mode == "L": # greyscale palette fp.write(o8(12)) From e6b7730c62992d0e7041b3a93ad87152959e76fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Jun 2022 19:49:02 +1000 Subject: [PATCH 034/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fee35799c52..34d96b0b29d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Fixed bug with rounding pixels to palette colors #6377 + [btrekkie, radarhere] + - Use gnome-screenshot on Linux if available #6361 [radarhere, nulano] From 2970a5b8b2b6a03e528f75128491a364fe54226a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Jun 2022 16:59:28 +1000 Subject: [PATCH 035/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 34d96b0b29d..18c2b6e2b55 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Pad PCX palette to 768 bytes when saving #6391 + [radarhere] + - Fixed bug with rounding pixels to palette colors #6377 [btrekkie, radarhere] From e5046b1057f24794ea83f694ecd23a404873853b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Apr 2022 08:53:27 +1000 Subject: [PATCH 036/242] Reverted to __array_interface__ with the release of NumPy 1.23 --- Tests/test_image_array.py | 8 +++++--- src/PIL/Image.py | 12 +++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 7168c4265bb..7e5fd6fe13c 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,4 +1,5 @@ import pytest +from packaging.version import parse as parse_version from PIL import Image @@ -34,9 +35,10 @@ def test_with_dtype(dtype): test_with_dtype(numpy.float64) test_with_dtype(numpy.uint8) - with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: - with pytest.raises(OSError): - numpy.array(im_truncated) + if parse_version(numpy.__version__) >= parse_version("1.23"): + with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: + with pytest.raises(OSError): + numpy.array(im_truncated) def test_fromarray(): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3cf4297351b..8d1b3e9568a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -671,14 +671,9 @@ def _repr_png_(self): raise ValueError("Could not save to PNG for display") from e return b.getvalue() - class _ArrayData: - def __init__(self, new): - self.__array_interface__ = new - - def __array__(self, dtype=None): + @property + def __array_interface__(self): # numpy array interface support - import numpy as np - new = {} shape, typestr = _conv_type_shape(self) new["shape"] = shape @@ -690,8 +685,7 @@ def __array__(self, dtype=None): new["data"] = self.tobytes("raw", "L") else: new["data"] = self.tobytes() - - return np.array(self._ArrayData(new), dtype) + return new def __getstate__(self): return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] From ef93892af8606b20c863c8987b01b328b706aa62 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Jun 2022 17:08:56 +1000 Subject: [PATCH 037/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 18c2b6e2b55..6fdd4bdd4fe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Reverted to __array_interface__ with the release of NumPy 1.23 #6394 + [radarhere] + - Pad PCX palette to 768 bytes when saving #6391 [radarhere] From 847ad8c512cf51c83740c8da621c8205fe693aa4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 26 Jun 2022 20:58:36 +1000 Subject: [PATCH 038/242] Clarify check that palette is not already at its smallest --- src/PIL/GifImagePlugin.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dd659c95905..ce9fb5dd46c 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -830,9 +830,13 @@ def _get_optimize(im, info): num_palette_colors = len(im.palette.palette) // Image.getmodebands( im.palette.mode ) - # Round up to power of 2 but at least 4 - num_palette_colors = max(4, 1 << (num_palette_colors - 1).bit_length()) - if len(used_palette_colors) <= num_palette_colors // 2: + current_palette_size = 1 << (num_palette_colors - 1).bit_length() + if ( + # check that the palette would become smaller when saved + len(used_palette_colors) <= current_palette_size // 2 + # check that the palette is not already the smallest possible size + and current_palette_size > 2 + ): return used_palette_colors From bc6a02422eed1ef798304f9c96d9410a210c6671 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Jun 2022 09:13:21 +1000 Subject: [PATCH 039/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6fdd4bdd4fe..4a0a86609b7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Improved GIF optimize condition #6378 + [raygard, radarhere] + - Reverted to __array_interface__ with the release of NumPy 1.23 #6394 [radarhere] From 93805d5257add6439c62992d83f691df9be8c5e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Jun 2022 14:03:20 +1000 Subject: [PATCH 040/242] Limit expected number of characters to nencoding --- .../fonts/10x20-ISO8859-1-fewer-characters.pcf | Bin 0 -> 25860 bytes Tests/test_font_pcf.py | 8 ++++++++ src/PIL/PcfFontFile.py | 12 ++++-------- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf diff --git a/Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf b/Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf new file mode 100644 index 0000000000000000000000000000000000000000..c065f59a9daca445b77e06a43daa7a0cdf645b62 GIT binary patch literal 25860 zcmeI5e{@^rdB-352Psg%6lj4!BPc%x)IlK$3BM96u@k%|wux;Zu+>sz*;bJyMY57O zkmM#YP2v!6*R`d;R@1IqyRNIdvLB^Crmbr`wyyO?**ew^?`&tM`(tNj=WJ_xPM3YZ z_ufZ-b0b|zmMQ<_IehfK_kExDd7t-rpL=yBnVY~+EOWky1i;1Mc!S8ZC{f78XdFZt zY!qqIV^k(^{3afQ_D+!>FB54o`rr8g#<=}0Ff`RK(&ip>?DRt-mV5jxCcoiP9AGY; z@2i-Uwjpoh3w?VFxQvzMAjrx)!5yq@03EEn3v6eF_I9zd8)Si84i2#L7vMN6RQ`Zg zK1t=@0l&)@OfN66<*&d?to$qZH()ROKUuj5{0v=zCU7e&>p(Xvec);8T!4x_#V{(< zZi@syx23zQuPeO0V_-+e*3RC(9Xmy~boY0)qq04bi^-Po_ReiP2dE-j68qwV@=!w*J1(?yiA{#W>h0y*=U0on2dHvxFb)*b&~^F<{Rjo9_?r zz=R8qclLI#U%&RQVn;{U=I(ZmTqO?=+;4j=o5MZDYIo1h9bAg^LbyNG6j`o4ht}WftF_;*O4+i%p#zum}y9S3) zU$t`eO2iIO`Zp^^Oq!3$?CW%l|gFu0q{)ymgfuTVwt5K!3R1 zU9g>#0b96#cA8_yZMgs&++v>?-0Fooj!ysl^lQ**^0d1Hy#@1LUbQACrzXz5oQuJ^ zJUDk6EbXNLmlyZ_^i}uT(}Qvv%;TM@4NQSKyz7U61!hGWgCGM6xRAAg2$&IR7SIEx z!Mw=0x5a0v4FXtw#`KKtbfn77zh5B3B9M0n=b!LT{1oVJuFps;ZHZTR| zL~aZL3(Sh#6a*Pi5V^SpM8J&5FA3-Y(_miYmNqa2=0t7{0SnBE+!h2GP{6%b3y6Rj zkyZgcU>eMetZV~QU{2)r5U{|kNGJ$0pdhlU1w_CM?%4$NfN3xC*1fC!io2@B`}(_mg?QyZ89b0V8VzyhgdNeQ9V%LZ3-G5&EV*TA_~yRgo-r4o%1^F`~l&ErbKc}9IM!8FOIKt;h1XkOnXV8dV6ZuucW@b8w<{T)N+g`+}k@*DyaGj*T8$)QA;F=Qem6U!IJ%KqkbjfCkg7Op?D8q{t=EP z%Pri472qRSjk=#)gHBxwOWwyY-_`^AI#dveSpfCDDDTCwpQsO7^7>Pc==r7g=$jYq zV+^S8Cp;%P-W;Rx(^C7WNBwF=bCLJF%oCQ{R;>+FPyJv?d*-pJw^Vh#MldI875evj ziu%T~k_0S#YXq1Jnm^|Gq4~qTt+c<4xw(%~*!7}&Og-xR3Hupeo@YtGV?QdzRee*R zxNgFAGgg|z5!A_I?5Qo5w5@Y--JIV~0_fzpWq|K<%K^v5T<8z`O>kINs2@SENeag~ zpleqjtMRWGKf|Wl({*ZDjXnCOe(0J?T~~BpD&=3%KiKv9L4UN3Wi|elvS*LQ_-jGH z<+>{ENeN-z5wDbM_v(3%%Z<}^zw4FepPv63_Xs9o?0L-dkNw;)>enLNZ?BYU^Xlkv z5S5aCSdKqlzp3vhrS_NWQagIBp$)TM&ZcQ^*up&r;Jt^!dypXz0jn(Mdni=P3g=Xt z-uz|Un2TP@8bikufb+);czn=3Hf>Rd?S7)Zaa;t7`M|zVhDm?Z8jW_?Y!_xk4R3;W*9vYRXFSyQFaDdsibS_Yy-J z;r+GWwF=$E+}AP8Wukd*=DNm%*u=Z-wF^wy`0bo`r=)q`d4ednEpjz zm+<|G?TvuG5YAUk{AxYlcux9>cb$I6)%fQf&oK(uteDq|TK&^})>6+Mox2))UZ3cX z`lfyL+B4T^*8TkB{svr_ALqGnJnyZ{`>hC$D_vjs9SHNSTBu)1)Ry|AYg1q8x0&01 z_BfyFS5sEn6FNN1!_SGuo{L|}81O`ni*{^gxV9uCSvd7Ql{Ea~0_q=7L@!oztM}Cwjj4xKgr9?Xfh^4W4VX zU#rlL`lUXZIldTsrjJ_J_ysVZIcFvN8==oJjZox07JcL%GY{G4C(F>UYck7y7-Sx6 zo%(*FdbN~(_Wb-PH3#+mgy$)9V%SgV4eZl3(FLtnDl56GYL8bwxtq1ujsqhX}j9BTmxO^gZF)L1C1;~y!L39_SwD|G5uQ1 z_-e5_ zJzUFqz`SmF^N;>Spks2-bId;Kl@i+LJj`iIz4pxbFNHnEf~eH%pPsv1H-))PpDR)B zKF%AzdhL1RSFb%Ye%0Ed59QX2E?bymF<_o!tp5~v+o(tVQsTY7aO`d{?lOv!47>3A zZM82Y#Jj)dd8gyNrJipk%oVTAdVbVv&%3|%+VjS*UVCP~RAY;=^ow7qm}(pE2bddr zeKYTw*jH98=l6QftADc)N1KvBU12U!Zv$upUh(FOH#bTh`>oe|Jt#Ha+vaD_n=jt6 zrH=jVv5!znjV;yYXRq9Pbspvr;X1sasjTpQPT{pkf5$OPwyO?HKjG($NzdeAt z$GWz&1{oVsAWyKrIdhL00r(S#BYfZiO7zoW1*6XpS*DBg$ zol=iI$HJMf_uz25<$TuSccJ`jm?UUhhq6{#fdS072uc&Sv5))5oY3nds%HpuUUQHg z8UyB?d7q^AsN*M%=+qpfUcL6b`B$$!`ef!yz4pBE<9^cTQZSyCL}RD-3iaCa#;;y` z^xuqMJ@(pRD+$)Su)YN_Z~1$cdF?aLNpzPL@AakJ@4srhtRc6s126Y`sa6k zEXJO9f9v(n8^3z(F^p!uEQLL;nW*r*GS4k7rs)29^`Ul_)UsZ$f6jB-4xgAjW#MJ=p#88m)Sl*{`*MV}BgZFCjic?v z!G#X%$;qkxcsVvPG3mVEP#^WR8?}>r@N$f8M-JiT0QIIOILv+=7CSK3KE&jf#TS@A zZu?>#o}7Hjnm9Ij{Bg@d=fr#AB;DA{jziFc&s4$7DZ6*yQS@3zjiFTqoC5|<9Du2+#+ICoK9}q`!zdlEM$a+D;5ZAk zYaGj1S%=~HsXaKf18dtWS)0(=PSVQB;^D#wdzkaW-sAQQC%4;97Hfu{-8KPxcAK+N zj=P*~&%)Lsu=ZX)W#<9+_oTDu)U!7}Z@2OA38acM+Y?yovHjZdbdD=`@|1b7-+pnH z551$dDLbXmVND!CO6}){K7laV_D6pjBXolPEp*s5yZ0$>_bG-f;3tzt{4~>yTk~c3N#uMyQ}RN*gjlAH1HuUq6+xeX6bZIzXBJ1()S@IbEB zvPRb8W5zn%&fO(<%RTsLb06L^H{vZbESvBc(00M2NM$R2R_c^r#ee!Gae))Ag4fccbAw2x{!}1aNsC-O5F25n4kZE~NJ}IA)-;__wZ^>`V zXXLYZQtapOq}b2P@5vWrM!qP&FJF@9L-~>X zQ!yW2MIy|d<6I2R<-jjK2i$8i&Ouvv8-6*s8e9)<1$ThEK^VLX^nyphAQ%Pws*%_9 z<6sIr3!Vd?1z!X&fNy~BfR_QzeEBi>Z}1cBc>r7h@DVSt0^9^{2beQ}IRl*la|Q-L z0*nFtiAUfFz?^|+zz4y{0Ok#R8es0g7r^u2%iwDOJ_KF_-v!?X@FVaq;NQV(;Ai0H z;52^8hffWSpcyO!=L7uQ-*6GY*JBNrg3G`a;B5fDH!KI&0<59oI`9r~Be)sd0&WAX z0BdVl1?~e|K_7^K$G~oo19`9)jDyF)0dNQ$21mh@;21aoPJ&YaacFoSct7|6_z?Io z_$c@|_yl+kdutyE2@mIKrL1PQJ8eo4K*MN;+ z3wQuL3qZJtpjZUv1;l9j{xLe6XMl`Kb>eg2#^O&n5zkEZ~6$pI-4+G)AIo9Yx*|8zBggs zrXK>#+w@=HrvU3~#@x-AyBQyon=yAY_NsX;xCd+in7bKsH}``mz}(H4yBYh_jJ;?^ z?3*!nGv;o_+|8c>n7jEa;F|!s-~1AI1^hGk5AeU>|M0D36S#OVJ`~L-$6|O&xa3ob zJJxQ#Q*tBG!MJ_2CUlouZg$Idw+zc*d^j7AOD37mjV4n0oYQf~8dqVH^lk~Q2|-v{ ztra{c+-bqX!=1`nyTZ9wZwlLkS8w8ATQlS~IMc7*)J|jTb?9AMS*Mjd-O4Iwo@6|i zi|@|k>Fw|}Iu_452bqMV(qm3Nht^~wH7uEUHjy5T#Zy!plw2kni%Wc8EEye@-Fc`F z?n+83Ke{WP%_W9YGMG*#5i|_r7>SOemcw(_9pxNMCJ`Mxd_9$n4~;psY+`r>s+=>C zd(4iV#L}aqQ5hM}jKou}6Ov0tb0e}po=rHSX&D<~w1?99tPCag#3h&5C%O0@ zjEUPuQwb=>(#doR&7%pY#`Vh?0f2Y$+*kry(XmC{hwjQoV~@qh>^1E|)p6Br)%C>^ ziC7{V!z1G#md?hp1yT6Dn0r@RkGnHw#Xd4-8cQURuI7FXruU}A-j(9+#`0P0 zL~LBf5wTs_^kebVF6>?|9|}a4 z5sM~~Lb`3)_;7+#A#X;bu`Hr{*4cqQ%;oJI%Q|~mJc`EhV{sYHGex2ZQf9=Pi!uE3 z1V}dKLU9z_8;#~-dE~C)pX+*xd!B_u95ND34mnf0jk#hH*ntW=8xwZ?2s;}$l#K6_ zuoK#FF*@Na&y;Y7Y*v>#oWUK=;0|Lj{%HX>rk##Xr=!zp=yVo!I>tt%%bBj*QSWxt zyAAbjSD`p+6ql| Date: Mon, 27 Jun 2022 15:50:31 +1000 Subject: [PATCH 041/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4a0a86609b7..8a37e6dbb56 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Handle PCF fonts files with less than 256 characters #6386 + [dawidcrivelli, radarhere] + - Improved GIF optimize condition #6378 [raygard, radarhere] From 3558f4e5d69074bd7bf424c116ac8244322e1366 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 28 Jun 2022 09:50:03 +1000 Subject: [PATCH 042/242] Updated harfbuzz to 4.4.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d7e88ca4075..0870cc22511 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -281,9 +281,9 @@ def cmd_msbuild( "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/4.3.0.zip", - "filename": "harfbuzz-4.3.0.zip", - "dir": "harfbuzz-4.3.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/4.4.0.zip", + "filename": "harfbuzz-4.4.0.zip", + "dir": "harfbuzz-4.4.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), From 31bed8cfccd29a0ac19ce8c36b93a91adbaa324b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 29 Jun 2022 09:49:14 +0300 Subject: [PATCH 043/242] Docs: remove redundant search page from table of contents --- docs/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 5e886c2e82a..c731e274600 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -94,4 +94,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` -* :ref:`search` From eebbf6b27174156d1be9aaaa768c316636120fac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Jun 2022 18:59:44 +1000 Subject: [PATCH 044/242] Updated harfbuzz to 4.4.1 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0870cc22511..2e7c84ad0e2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -281,9 +281,9 @@ def cmd_msbuild( "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/4.4.0.zip", - "filename": "harfbuzz-4.4.0.zip", - "dir": "harfbuzz-4.4.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/4.4.1.zip", + "filename": "harfbuzz-4.4.1.zip", + "dir": "harfbuzz-4.4.1", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), From c9f1b35e981075110a23487a8d4a6cbb59a588ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Jun 2022 12:47:35 +1000 Subject: [PATCH 045/242] Added GIF decompression bomb check --- Tests/images/decompression_bomb_extents.gif | Bin 0 -> 368 bytes Tests/test_decompression_bomb.py | 5 +++++ src/PIL/GifImagePlugin.py | 1 + 3 files changed, 6 insertions(+) create mode 100644 Tests/images/decompression_bomb_extents.gif diff --git a/Tests/images/decompression_bomb_extents.gif b/Tests/images/decompression_bomb_extents.gif new file mode 100644 index 0000000000000000000000000000000000000000..0d5ff03f525904d4bc29e3b22f70b45eb1c2cba7 GIT binary patch literal 368 zcmZ?wbh9u|Okqf2XkcLY&j12CAOa-9z_E~lkweB~!-9j&9Ku>LCpIiR+%BN(HOFJ) zqNCjs##whxY+QW2U%|Oc#&gq>lan=qSH+y%wDk0JgXB|lJU1^pJKLi8)t!@@m!F^S z(99+4wPnS{#U7KjVoz;Zd3kxjVz0SgTUT9O9kDsd1uK)l5 literal 0 HcmV?d00001 diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index d85d1f3c266..63071b78c9c 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -61,6 +61,11 @@ def test_exception_gif(self): with Image.open("Tests/images/decompression_bomb.gif"): pass + def test_exception_gif_extents(self): + with Image.open("Tests/images/decompression_bomb_extents.gif") as im: + with pytest.raises(Image.DecompressionBombError): + im.seek(1) + def test_exception_bmp(self): with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/bmp/b/reallybig.bmp"): diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index ce9fb5dd46c..c239a6a2bdb 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -265,6 +265,7 @@ def _seek(self, frame, update_image=True): x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) if (x1 > self.size[0] or y1 > self.size[1]) and update_image: self._size = max(x1, self.size[0]), max(y1, self.size[1]) + Image._decompression_bomb_check(self._size) frame_dispose_extent = x0, y0, x1, y1 flags = s[8] From 7527964ce94b1018362271a82bd09ee98fd7a455 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Jun 2022 18:46:51 +1000 Subject: [PATCH 046/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8a37e6dbb56..a8eabab8752 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Added GIF decompression bomb check #6402 + [radarhere] + - Handle PCF fonts files with less than 256 characters #6386 [dawidcrivelli, radarhere] From 3b8e195db2f7bbee98441849849b845704d7d393 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Jun 2022 18:58:38 +1000 Subject: [PATCH 047/242] Added release notes for #6402 --- docs/releasenotes/9.2.0.rst | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 20e6cfa950f..ca52f6ab952 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -1,12 +1,6 @@ 9.2.0 ----- -Backwards Incompatible Changes -============================== - -TODO -^^^^ - Deprecations ============ @@ -46,14 +40,6 @@ Image.coerce_e This undocumented method has been deprecated and will be removed in Pillow 10 (2023-07-01). -API Changes -=========== - -TODO -^^^^ - -TODO - API Additions ============= @@ -68,10 +54,7 @@ The image's palette mode will become "RGBA", and "transparency" will be removed Security ======== -TODO -^^^^ - -TODO +An additional decompression bomb check has been added for the GIF format. Other Changes ============= From 31009c978094941a75a2f7c6fb269f2ea9d3063f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Jun 2022 19:30:01 +1000 Subject: [PATCH 048/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a8eabab8752..c9b4a9f491f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Fixed null check for fribidi_version_info in FriBiDi shim #6376 + [nulano] + - Added GIF decompression bomb check #6402 [radarhere] From f57a9d678c770712320345ed9f71c04b38db1249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 30 Jun 2022 14:20:56 +0200 Subject: [PATCH 049/242] update TransposedFont.getlength error message Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ef452778266..77d2cf0f9da 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -865,7 +865,7 @@ def getbbox(self, text, *args, **kwargs): def getlength(self, text, *args, **kwargs): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - raise ValueError("text length is undefined for rotated text") + raise ValueError("text length is undefined for text rotated by 90 or 270 degrees") return self.font.getlength(text, *args, **kwargs) From 303ec1a95e8091c1fa3c0a11da7e8603c55e6a69 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 12:21:39 +0000 Subject: [PATCH 050/242] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/ImageFont.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 77d2cf0f9da..8a55d517fdc 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -865,7 +865,9 @@ def getbbox(self, text, *args, **kwargs): def getlength(self, text, *args, **kwargs): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - raise ValueError("text length is undefined for text rotated by 90 or 270 degrees") + raise ValueError( + "text length is undefined for text rotated by 90 or 270 degrees" + ) return self.font.getlength(text, *args, **kwargs) From 838b1f1598f5f7c110874a5341dd01aba611ddd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 30 Jun 2022 14:22:07 +0200 Subject: [PATCH 051/242] add replacement for getsize_multiline to deprecation warning --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 8a55d517fdc..0bd92749d58 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -532,7 +532,7 @@ def getsize_multiline( :return: (width, height) """ - deprecate("getsize_multiline", 10) + deprecate("getsize_multiline", 10, "ImageDraw.multiline_textbbox") max_width = 0 lines = self._multiline_split(text) line_spacing = ( From 3c0b8763abb26232bf304a5cdf2b046d5f7e443e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Jun 2022 21:58:47 +1000 Subject: [PATCH 052/242] Added documentation and release notes --- docs/deprecations.rst | 16 ++++++++++++++++ docs/reference/ImageFont.rst | 1 + docs/releasenotes/9.2.0.rst | 16 ++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8c5b8a748d7..af9c35f34a4 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -178,6 +178,22 @@ Image.coerce_e This undocumented method has been deprecated and will be removed in Pillow 10 (2023-07-01). +Font size and offset methods +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +=========================================================================== ============================================================================================================= +Deprecated Use instead +=========================================================================== ============================================================================================================= +:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw().textsize`` ``ImageDraw2.Draw().textbbox`` and ``ImageDraw2.Draw().textlength`` +=========================================================================== ============================================================================================================= + Removed features ---------------- diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 8efef7cfd5c..516fa63a783 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -56,6 +56,7 @@ Methods .. autoclass:: PIL.ImageFont.TransposedFont :members: + :undoc-members: Constants --------- diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index ca52f6ab952..8815e2c0d61 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -40,6 +40,22 @@ Image.coerce_e This undocumented method has been deprecated and will be removed in Pillow 10 (2023-07-01). +Font size and offset methods +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.2.0 + +=========================================================================== ============================================================================================================= +Deprecated Use instead +=========================================================================== ============================================================================================================= +:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength` +:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox` +:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` +:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` +:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` +``ImageDraw2.Draw().textsize`` ``ImageDraw2.Draw().textbbox`` and ``ImageDraw2.Draw().textlength`` +=========================================================================== ============================================================================================================= + API Additions ============= From 9957e0b0d7371685abcc68f6d591277d26348fc9 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 30 Jun 2022 14:38:20 +0200 Subject: [PATCH 053/242] link to ImageDraw2 in deprecations and release notes --- docs/deprecations.rst | 2 +- docs/releasenotes/9.2.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index af9c35f34a4..3796e3dfbeb 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -191,7 +191,7 @@ Deprecated Use :py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` :py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` :py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` -``ImageDraw2.Draw().textsize`` ``ImageDraw2.Draw().textbbox`` and ``ImageDraw2.Draw().textlength`` +:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= Removed features diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 8815e2c0d61..bd6b767e777 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -53,7 +53,7 @@ Deprecated Use :py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength` :py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength` :py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox` -``ImageDraw2.Draw().textsize`` ``ImageDraw2.Draw().textbbox`` and ``ImageDraw2.Draw().textlength`` +:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= API Additions From 7691231aa7bb74584139c8c50b60e93d56790f14 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 30 Jun 2022 14:51:29 +0200 Subject: [PATCH 054/242] Fix heading in deprecations.rst --- docs/deprecations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 3796e3dfbeb..a0289bb24f2 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -179,7 +179,7 @@ This undocumented method has been deprecated and will be removed in Pillow 10 (2023-07-01). Font size and offset methods -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 9.2.0 From 65020e7c7c4539e905b3f5a9a1666892ac41ed70 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Jun 2022 22:40:40 +1000 Subject: [PATCH 055/242] Documented deprecation in individual methods --- docs/reference/ImageDraw.rst | 4 ++++ src/PIL/ImageDraw2.py | 2 ++ src/PIL/ImageFont.py | 11 +++++++++++ 3 files changed, 17 insertions(+) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index b95d8d591a7..1ba024eea9d 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -436,6 +436,8 @@ Methods .. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) + .. deprecated:: 9.2.0 + Return the size of the given string, in pixels. Use :py:meth:`textlength()` to measure the offset of following text with @@ -484,6 +486,8 @@ Methods .. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) + .. deprecated:: 9.2.0 + Return the size of the given string, in pixels. Use :py:meth:`textlength()` to measure the offset of following text with diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 6e6a307bde5..ea9461e92ec 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -173,6 +173,8 @@ def text(self, xy, text, font): def textsize(self, text, font): """ + .. deprecated:: 9.2.0 + Return the size of the given string, in pixels. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize` diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 0bd92749d58..1f356adefd9 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -137,6 +137,8 @@ def _load_pilfont_data(self, file, image): def getsize(self, text, *args, **kwargs): """ + .. deprecated:: 9.2.0 + Returns width and height (in pixels) of given text. :param text: Text to measure. @@ -424,6 +426,8 @@ def getsize( __internal__=False, ): """ + .. deprecated:: 9.2.0 + Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language. @@ -493,6 +497,8 @@ def getsize_multiline( stroke_width=0, ): """ + .. deprecated:: 9.2.0 + Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language, while respecting newline characters. @@ -548,6 +554,8 @@ def getsize_multiline( def getoffset(self, text): """ + .. deprecated:: 9.2.0 + Returns the offset of given text. This is the gap between the starting coordinate and the first marking. Note that this gap is included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`. @@ -837,6 +845,9 @@ def __init__(self, font, orientation=None): self.orientation = orientation # any 'transpose' argument, or None def getsize(self, text, *args, **kwargs): + """ + .. deprecated:: 9.2.0 + """ if not kwargs.get("__internal__"): deprecate("getsize", 10, "getbbox or getlength") try: From ad5271d62f65deb67b10c1938fff782e68ab3e62 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 30 Jun 2022 15:02:09 +0200 Subject: [PATCH 056/242] Document replacements for individual deprecated font methods --- docs/reference/ImageDraw.rst | 8 ++++---- src/PIL/ImageFont.py | 11 +++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 1ba024eea9d..e6e2d15c06b 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -437,13 +437,12 @@ Methods .. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) .. deprecated:: 9.2.0 + Use :py:meth:`textlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. Return the size of the given string, in pixels. - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. - .. note:: For historical reasons this function measures text height from the ascender line instead of the top, see :ref:`text-anchors`. If you wish to measure text height from the top, it is recommended @@ -487,6 +486,7 @@ Methods .. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) .. deprecated:: 9.2.0 + Use :py:meth:`.multiline_textbbox` instead. Return the size of the given string, in pixels. diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 1f356adefd9..8c23b59b6be 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -138,6 +138,7 @@ def _load_pilfont_data(self, file, image): def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. Returns width and height (in pixels) of given text. @@ -427,14 +428,13 @@ def getsize( ): """ .. deprecated:: 9.2.0 + Use :py:meth:`getlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language. - Use :py:meth:`getlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. - .. note:: For historical reasons this function measures text height from the ascender line instead of the top, see :ref:`text-anchors`. If you wish to measure text height from the top, it is recommended @@ -498,6 +498,7 @@ def getsize_multiline( ): """ .. deprecated:: 9.2.0 + Use :py:meth:`.ImageDraw.multiline_textbbox` instead. Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language, while respecting @@ -555,6 +556,7 @@ def getsize_multiline( def getoffset(self, text): """ .. deprecated:: 9.2.0 + Use :py:meth:`.getbbox` instead. Returns the offset of given text. This is the gap between the starting coordinate and the first marking. Note that this gap is @@ -847,6 +849,7 @@ def __init__(self, font, orientation=None): def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. """ if not kwargs.get("__internal__"): deprecate("getsize", 10, "getbbox or getlength") From a37c21e136fbd0398a9f767b0cc7ac910ce67daf Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 30 Jun 2022 15:29:09 +0200 Subject: [PATCH 057/242] document planned removal date for ImageFont deprecations and release notes --- docs/deprecations.rst | 3 +++ docs/releasenotes/9.2.0.rst | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a0289bb24f2..9be92770ab9 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -183,6 +183,9 @@ Font size and offset methods .. deprecated:: 9.2.0 +Several functions for computing the size and offset of rendered text +have been deprecated and will be removed in Pillow 10 (2023-07-01): + =========================================================================== ============================================================================================================= Deprecated Use instead =========================================================================== ============================================================================================================= diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index bd6b767e777..9c102f1776a 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -45,6 +45,9 @@ Font size and offset methods .. deprecated:: 9.2.0 +Several functions for computing the size and offset of rendered text +have been deprecated and will be removed in Pillow 10 (2023-07-01): + =========================================================================== ============================================================================================================= Deprecated Use instead =========================================================================== ============================================================================================================= From 74e0b954f2010b0e2e19166b1defad2f4ed6307c Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 1 Jul 2022 11:22:47 +0200 Subject: [PATCH 058/242] test {ImageFont,TransposedFont}.getsize() deprecation --- Tests/test_font_pcf.py | 3 +++ Tests/test_imagefont.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 815ef1d9254..c217378fb74 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -82,6 +82,9 @@ def test_textsize(request, tmp_path): assert dy == 20 assert dx in (0, 10) assert font.getlength(chr(i)) == dx + with pytest.warns(DeprecationWarning) as log: + assert font.getsize(chr(i)) == (dx, dy) + assert len(log) == 1 for i in range(len(message)): msg = message[: i + 1] assert font.getlength(msg) == len(msg) * 10 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 606b9ba0ed4..16da87d469a 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -317,14 +317,16 @@ def test_rotated_transposed_font(self): draw.font = font with pytest.warns(DeprecationWarning) as log: box_size_a = draw.textsize(word) - assert len(log) == 1 + assert box_size_a == font.getsize(word) + assert len(log) == 2 bbox_a = draw.textbbox((10, 10), word) # Rotated font draw.font = transposed_font with pytest.warns(DeprecationWarning) as log: box_size_b = draw.textsize(word) - assert len(log) == 1 + assert box_size_b == transposed_font.getsize(word) + assert len(log) == 2 bbox_b = draw.textbbox((20, 20), word) # Check (w,h) of box a is (h,w) of box b From 729fe6f8b0ec7ebea408c526d4c3f8c998c7e6bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Jul 2022 19:45:36 +1000 Subject: [PATCH 059/242] Updated indentation --- docs/reference/ImageDraw.rst | 10 ++++++---- src/PIL/ImageFont.py | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index e6e2d15c06b..c2d72c804c1 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -437,9 +437,10 @@ Methods .. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) .. deprecated:: 9.2.0 - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. + + Use :py:meth:`textlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. Return the size of the given string, in pixels. @@ -486,7 +487,8 @@ Methods .. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) .. deprecated:: 9.2.0 - Use :py:meth:`.multiline_textbbox` instead. + + Use :py:meth:`.multiline_textbbox` instead. Return the size of the given string, in pixels. diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 8c23b59b6be..1f62468ab3b 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -138,7 +138,8 @@ def _load_pilfont_data(self, file, image): def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. + + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. Returns width and height (in pixels) of given text. @@ -428,9 +429,10 @@ def getsize( ): """ .. deprecated:: 9.2.0 - Use :py:meth:`getlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. + + Use :py:meth:`getlength()` to measure the offset of following text with + 1/64 pixel precision. + Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language. @@ -498,7 +500,8 @@ def getsize_multiline( ): """ .. deprecated:: 9.2.0 - Use :py:meth:`.ImageDraw.multiline_textbbox` instead. + + Use :py:meth:`.ImageDraw.multiline_textbbox` instead. Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language, while respecting @@ -556,7 +559,8 @@ def getsize_multiline( def getoffset(self, text): """ .. deprecated:: 9.2.0 - Use :py:meth:`.getbbox` instead. + + Use :py:meth:`.getbbox` instead. Returns the offset of given text. This is the gap between the starting coordinate and the first marking. Note that this gap is @@ -849,7 +853,8 @@ def __init__(self, font, orientation=None): def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. + + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. """ if not kwargs.get("__internal__"): deprecate("getsize", 10, "getbbox or getlength") From 4ca99f74545182b5184fa16ab69db9cbf0fb7d3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Jul 2022 19:51:29 +1000 Subject: [PATCH 060/242] Install furo if it is not available --- docs/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Makefile b/docs/Makefile index 0d352302f93..309fc4458e0 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -43,6 +43,7 @@ clean: install-sphinx: python3 -c "import sphinx" > /dev/null 2>&1 || python3 -m pip install sphinx + python3 -c "import furo" > /dev/null 2>&1 || python3 -m pip install furo html: $(MAKE) install-sphinx From 8a6050ee5bef46f3c77688023d21b680d74b8af2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Jul 2022 20:33:59 +1000 Subject: [PATCH 061/242] Replaced __internal__ argument with warning filters --- src/PIL/ImageDraw.py | 102 +++++++++++++++++++++--------------------- src/PIL/ImageDraw2.py | 6 ++- src/PIL/ImageFont.py | 31 ++++++------- 3 files changed, 70 insertions(+), 69 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 723ec17fdc1..8970471d3b2 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -32,6 +32,7 @@ import math import numbers +import warnings from . import Image, ImageColor from ._deprecate import deprecate @@ -375,15 +376,16 @@ def _multiline_split(self, text): def _multiline_spacing(self, font, spacing, stroke_width): # this can be replaced with self.textbbox(...)[3] when textsize is removed - return ( - self.textsize( - "A", - font=font, - stroke_width=stroke_width, - __internal__=True, - )[1] - + spacing - ) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return ( + self.textsize( + "A", + font=font, + stroke_width=stroke_width, + )[1] + + spacing + ) def text( self, @@ -582,34 +584,34 @@ def textsize( features=None, language=None, stroke_width=0, - __internal__=False, ): """Get the size of a given string, in pixels.""" - if not __internal__: - deprecate("textsize", 10, "textbbox or textlength") + deprecate("textsize", 10, "textbbox or textlength") if self._multiline_check(text): - return self.multiline_textsize( + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return self.multiline_textsize( + text, + font, + spacing, + direction, + features, + language, + stroke_width, + ) + + if font is None: + font = self.getfont() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return font.getsize( text, - font, - spacing, direction, features, language, stroke_width, - __internal__=True, ) - if font is None: - font = self.getfont() - return font.getsize( - text, - direction, - features, - language, - stroke_width, - __internal__=True, - ) - def multiline_textsize( self, text, @@ -619,25 +621,24 @@ def multiline_textsize( features=None, language=None, stroke_width=0, - __internal__=False, ): - if not __internal__: - deprecate("multiline_textsize", 10, "multiline_textbbox") + deprecate("multiline_textsize", 10, "multiline_textbbox") max_width = 0 lines = self._multiline_split(text) line_spacing = self._multiline_spacing(font, spacing, stroke_width) - for line in lines: - line_width, line_height = self.textsize( - line, - font, - spacing, - direction, - features, - language, - stroke_width, - __internal__=True, - ) - max_width = max(max_width, line_width) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + for line in lines: + line_width, line_height = self.textsize( + line, + font, + spacing, + direction, + features, + language, + stroke_width, + ) + max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing def textlength( @@ -662,14 +663,15 @@ def textlength( return font.getlength(text, mode, direction, features, language) except AttributeError: deprecate("textlength support for fonts without getlength", 10) - size = self.textsize( - text, - font, - direction=direction, - features=features, - language=language, - __internal__=True, - ) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + size = self.textsize( + text, + font, + direction=direction, + features=features, + language=language, + ) if direction == "ttb": return size[1] return size[0] diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index ea9461e92ec..2667b77dd43 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -24,6 +24,8 @@ """ +import warnings + from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath from ._deprecate import deprecate @@ -180,7 +182,9 @@ def textsize(self, text, font): .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize` """ deprecate("textsize", 10, "textbbox or textlength") - return self.draw.textsize(text, font=font.font, __internal__=True) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return self.draw.textsize(text, font=font.font) def textbbox(self, xy, text, font): """ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 1f62468ab3b..a3b711c6077 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -147,8 +147,7 @@ def getsize(self, text, *args, **kwargs): :return: (width, height) """ - if not kwargs.get("__internal__"): - deprecate("getsize", 10, "getbbox or getlength") + deprecate("getsize", 10, "getbbox or getlength") return self.font.getsize(text) def getmask(self, text, mode="", *args, **kwargs): @@ -425,7 +424,6 @@ def getsize( features=None, language=None, stroke_width=0, - __internal__=False, ): """ .. deprecated:: 9.2.0 @@ -479,8 +477,7 @@ def getsize( :return: (width, height) """ - if not __internal__: - deprecate("getsize", 10, "getbbox or getlength") + deprecate("getsize", 10, "getbbox or getlength") # vertical offset is added for historical reasons # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 size, offset = self.font.getsize(text, "L", direction, features, language) @@ -545,14 +542,14 @@ def getsize_multiline( deprecate("getsize_multiline", 10, "ImageDraw.multiline_textbbox") max_width = 0 lines = self._multiline_split(text) - line_spacing = ( - self.getsize("A", stroke_width=stroke_width, __internal__=True)[1] + spacing - ) - for line in lines: - line_width, line_height = self.getsize( - line, direction, features, language, stroke_width, __internal__=True - ) - max_width = max(max_width, line_width) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing + for line in lines: + line_width, line_height = self.getsize( + line, direction, features, language, stroke_width + ) + max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing @@ -856,11 +853,9 @@ def getsize(self, text, *args, **kwargs): Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. """ - if not kwargs.get("__internal__"): - deprecate("getsize", 10, "getbbox or getlength") - try: - w, h = self.font.getsize(text, __internal__=True) - except TypeError: + deprecate("getsize", 10, "getbbox or getlength") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) w, h = self.font.getsize(text) if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): return h, w From dc518ac25e912200dd885728f5dbc885c812f8f2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 1 Jul 2022 16:16:26 +0300 Subject: [PATCH 062/242] 9.2.0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 66cb160645f..3b67ed5770a 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.2.0.dev0" +__version__ = "9.2.0" From 58acec3312fb8671c9d84829197e1c8150085589 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 1 Jul 2022 16:19:51 +0300 Subject: [PATCH 063/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c9b4a9f491f..d8419f7120a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,12 @@ Changelog (Pillow) ================== -9.2.0 (unreleased) +9.2.0 (2022-07-01) ------------------ +- Deprecate ImageFont.getsize and related functions #6381 + [nulano, radarhere] + - Fixed null check for fribidi_version_info in FriBiDi shim #6376 [nulano] From 26e2c7e030bac7c28ff0bd63dc7bfdaecd99f83c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 1 Jul 2022 21:29:53 +0300 Subject: [PATCH 064/242] 9.3.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 3b67ed5770a..8e736a4326f 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.2.0" +__version__ = "9.3.0.dev0" From 43b185e1c1b011792770ebcacdc68ca7d58ee291 Mon Sep 17 00:00:00 2001 From: neilnaveen <42328488+neilnaveen@users.noreply.github.com> Date: Mon, 4 Jul 2022 01:19:58 +0000 Subject: [PATCH 065/242] chore: Set permissions for GitHub actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restrict the GitHub token permissions only to the required ones; this way, even if the attackers will succeed in compromising your workflow, they won’t be able to do much. - Included permissions for the action. https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs [Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/) Signed-off-by: neilnaveen <42328488+neilnaveen@users.noreply.github.com> --- .github/workflows/lint.yml | 3 +++ .github/workflows/release-drafter.yml | 6 ++++++ .github/workflows/test-cygwin.yml | 2 ++ .github/workflows/test-docker.yml | 5 +++++ .github/workflows/test-mingw.yml | 5 +++++ .github/workflows/test-valgrind.yml | 3 +++ .github/workflows/test-windows.yml | 5 +++++ .github/workflows/test.yml | 5 +++++ .github/workflows/tidelift.yml | 3 +++ 9 files changed, 37 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4540fb5afd2..527f26d35d4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,6 +2,9 @@ name: Lint on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: build: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index ad66117b187..7ee76c4ac8c 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -7,8 +7,14 @@ on: - main workflow_dispatch: +permissions: + contents: read + jobs: update_release_draft: + permissions: + contents: write # for release-drafter/release-drafter to create a github release + pull-requests: write # for release-drafter/release-drafter to add label to PR if: github.repository == 'python-pillow/Pillow' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 2e8fc9c09f0..417b1f21276 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -99,6 +99,8 @@ jobs: name: Cygwin Python 3.${{ matrix.python-minor-version }} success: + permissions: + contents: none needs: build runs-on: ubuntu-latest name: Cygwin Test Successful diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 2b4dc6b5232..a789726079e 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -2,6 +2,9 @@ name: Test Docker on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: build: @@ -82,6 +85,8 @@ jobs: name: ${{ matrix.docker }} success: + permissions: + contents: none needs: build runs-on: ubuntu-latest name: Docker Test Successful diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 7b5cc8a972e..7ddb71e1f68 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -2,6 +2,9 @@ name: Test MinGW on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: build: runs-on: windows-latest @@ -77,6 +80,8 @@ jobs: CODECOV_NAME: ${{ matrix.name }} success: + permissions: + contents: none needs: build runs-on: ubuntu-latest name: MinGW Test Successful diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 21a2b469ee0..013e5ca4ac3 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -13,6 +13,9 @@ on: - "**.h" workflow_dispatch: +permissions: + contents: read + jobs: build: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 358461b388b..b9accfdf9a5 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -2,6 +2,9 @@ name: Test Windows on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: build: runs-on: windows-latest @@ -189,6 +192,8 @@ jobs: path: dist\*.whl success: + permissions: + contents: none needs: build runs-on: ubuntu-latest name: Windows Test Successful diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d41f4b57196..5614ad5f228 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,9 @@ name: Test on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: build: @@ -109,6 +112,8 @@ jobs: CODECOV_NAME: ${{ matrix.os }} Python ${{ matrix.python-version }} success: + permissions: + contents: none needs: build runs-on: ubuntu-latest name: Test Successful diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml index 9a3192f9d7d..c73f254313d 100644 --- a/.github/workflows/tidelift.yml +++ b/.github/workflows/tidelift.yml @@ -12,6 +12,9 @@ on: - ".github/workflows/tidelift.yml" workflow_dispatch: +permissions: + contents: read + jobs: build: if: github.repository_owner == 'python-pillow' From 300f6c8a59dbfd2df35715cf86be5869532371cb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 4 Jul 2022 19:33:21 +1000 Subject: [PATCH 066/242] Document where to find other codecs --- docs/handbook/writing-your-own-image-plugin.rst | 4 ++++ src/PIL/Image.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 80138742d68..323127e5b04 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -141,6 +141,10 @@ The fields are used as follows: uncompressed data, in a variety of pixel formats. For more information on this decoder, see the description below. + A list of C decoders can be seen under codecs section of the function array + in :file:`_imaging.c`. Python decoders are registered within the relevant + plugins. + **region** A 4-tuple specifying where to store data in the image. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 8d1b3e9568a..ce32c39ced6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -715,6 +715,11 @@ def tobytes(self, encoder_name="raw", *args): :param encoder_name: What encoder to use. The default is to use the standard "raw" encoder. + + A list of C encoders can be seen under + codecs section of the function array in + :file:`_imaging.c`. Python encoders are + registered within the relevant plugins. :param args: Extra arguments to the encoder. :returns: A :py:class:`bytes` object. """ From a0b22d22038fd9d858a8bcdf64e5f67d016e4555 Mon Sep 17 00:00:00 2001 From: Santiago Castro Date: Mon, 4 Jul 2022 09:50:47 -0700 Subject: [PATCH 067/242] Fix when `sys.executable` is empty or null --- src/PIL/ImageShow.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 9117f57e570..5ca8035b15b 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -178,14 +178,15 @@ def show_file(self, path=None, **options): else: raise TypeError("Missing required argument: 'path'") subprocess.call(["open", "-a", "Preview.app", path]) - subprocess.Popen( - [ - sys.executable, - "-c", - "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", - path, - ] - ) + if sys.executable: + subprocess.Popen( + [ + sys.executable, + "-c", + "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", + path, + ] + ) return 1 From 431498de0bc5d0e4119116e654db3177cf0b7ddf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 17:57:12 +0000 Subject: [PATCH 068/242] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0) - [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0) - [github.com/sphinx-contrib/sphinx-lint: v0.6 → v0.6.1](https://github.com/sphinx-contrib/sphinx-lint/compare/v0.6...v0.6.1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1806db54cdb..e5e1f3557f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black args: ["--target-version", "py37"] @@ -37,13 +37,13 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: check-merge-conflict - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.6 + rev: v0.6.1 hooks: - id: sphinx-lint From 527eecae80f41965ebf6aa3847baecaa41b1158c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Jul 2022 13:42:41 +1000 Subject: [PATCH 069/242] Fallback to python3 --- src/PIL/ImageShow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 5ca8035b15b..1b2b9f05fbf 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -178,10 +178,11 @@ def show_file(self, path=None, **options): else: raise TypeError("Missing required argument: 'path'") subprocess.call(["open", "-a", "Preview.app", path]) - if sys.executable: + exectable = sys.executable or shutil.which("python3") + if executable: subprocess.Popen( [ - sys.executable, + executable, "-c", "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", path, From 4f7f5470b13b4006db58f2e2396ea0f17c3de373 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Jul 2022 08:30:24 +1000 Subject: [PATCH 070/242] Fixed typo --- src/PIL/ImageShow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 1b2b9f05fbf..9f9a551fb6f 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -178,7 +178,7 @@ def show_file(self, path=None, **options): else: raise TypeError("Missing required argument: 'path'") subprocess.call(["open", "-a", "Preview.app", path]) - exectable = sys.executable or shutil.which("python3") + executable = sys.executable or shutil.which("python3") if executable: subprocess.Popen( [ From fdf4084a7f8ea49bca25158930a8af80cf685b81 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 7 Jul 2022 12:43:24 +1000 Subject: [PATCH 071/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d8419f7120a..f3ba8093fd5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (2022-07-01) ------------------ +- Handle missing Python executable in ImageShow on macOS #6416 + [bryant1410, radarhere] + - Deprecate ImageFont.getsize and related functions #6381 [nulano, radarhere] From 5c3077560b51c2c8f4b394764636dc06c5726442 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Jul 2022 14:38:40 +1000 Subject: [PATCH 072/242] Updated macOS tested Pillow versions [ci skip] --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c650d10cc6f..b4cee7b6900 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -504,11 +504,11 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.1.1 |arm | +| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | +---------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10 | 9.1.1 |x86-64 | +| | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |x86-64 | | +---------------------------+------------------+ | | | 3.6 | 8.4.0 | | +----------------------------------+---------------------------+------------------+--------------+ From 415d5b277cc590b492e0a615cc0d45f83da5866b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 11 Jul 2022 06:24:56 +1000 Subject: [PATCH 073/242] Updated redirected URL --- docs/releasenotes/9.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index dd993d39eb4..a19da361aaa 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -45,7 +45,7 @@ Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). -.. _FreeType: https://www.freetype.org +.. _FreeType: https://freetype.org/ Image.show command parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 23af975a41af11da9ece5a738093438110a892a1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 11 Jul 2022 06:04:04 +1000 Subject: [PATCH 074/242] Updated link for more information about SPIDER --- docs/handbook/image-file-formats.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1826d965f7a..30452c4a6c2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -827,11 +827,8 @@ the output format must be specified explicitly:: im.save('newimage.spi', format='SPIDER') -For more information about the SPIDER image processing package, see the -`SPIDER homepage`_ at `Wadsworth Center`_. - -.. _SPIDER homepage: https://spider.wadsworth.org/spider_doc/spider/docs/spider.html -.. _Wadsworth Center: https://www.wadsworth.org/ +For more information about the SPIDER image processing package, see +https://github.com/spider-em/SPIDER TGA ^^^ From ed98c668ee2215cf4717f57294e1894b19bd90ed Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 11 Jul 2022 06:54:23 +1000 Subject: [PATCH 075/242] Fixed PSDraw rectangle --- src/PIL/PSDraw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 743c35f0128..7d652020a6a 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -93,9 +93,9 @@ def rectangle(self, box): .. code-block:: python - %d %d M %d %d 0 Vr\n + %d %d M 0 %d %d Vr\n """ - self.fp.write(b"%d %d M %d %d 0 Vr\n" % box) + self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) def text(self, xy, text): """ From d6458f21d731561f1ce03bc3145a70042b30d3bc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 11 Jul 2022 12:29:03 +1000 Subject: [PATCH 076/242] Corrected CHANGES.rst [ci skip] --- CHANGES.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index f3ba8093fd5..4b313fcb9e2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,12 +2,15 @@ Changelog (Pillow) ================== -9.2.0 (2022-07-01) +9.3.0 (unreleased) ------------------ - Handle missing Python executable in ImageShow on macOS #6416 [bryant1410, radarhere] +9.2.0 (2022-07-01) +------------------ + - Deprecate ImageFont.getsize and related functions #6381 [nulano, radarhere] From db1d945ce771d0efff89c65052aa9277a6928738 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 11 Jul 2022 18:36:27 +1000 Subject: [PATCH 077/242] Changed second rectangle parameter to distance from bottom --- src/PIL/PSDraw.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 7d652020a6a..dd643c15cc5 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -86,14 +86,8 @@ def rectangle(self, box): """ Draws a rectangle. - :param box: A 4-tuple of integers whose order and function is currently - undocumented. - - Hint: the tuple is passed into this format string: - - .. code-block:: python - - %d %d M 0 %d %d Vr\n + :param box: A tuple of four integers, specifying left, bottom, width and + height. """ self.fp.write(b"%d %d M 0 %d %d Vr\n" % box) @@ -188,9 +182,9 @@ def image(self, box, im, dpi=None): /Vl { moveto lineto stroke } bind def /Vc { newpath 0 360 arc closepath } bind def /Vr { exch dup 0 rlineto - exch dup neg 0 exch rlineto + exch dup 0 exch rlineto exch neg 0 rlineto - 0 exch rlineto + 0 exch neg rlineto 100 div setgray fill 0 setgray } bind def /Tm matrix def /Ve { Tm currentmatrix pop From 80c1ef5dcdb03bc692801930a6801b7a917838a3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 11 Jul 2022 19:38:46 +1000 Subject: [PATCH 078/242] Simplified rectangle command --- src/PIL/PSDraw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index dd643c15cc5..13b3048f67e 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -185,7 +185,7 @@ def image(self, box, im, dpi=None): exch dup 0 exch rlineto exch neg 0 rlineto 0 exch neg rlineto - 100 div setgray fill 0 setgray } bind def + setgray fill } bind def /Tm matrix def /Ve { Tm currentmatrix pop translate scale newpath 0 0 .5 0 360 arc closepath From 40a09993036618b756c4b0c050614b1438ae7d84 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 11 Jul 2022 20:02:41 +1000 Subject: [PATCH 079/242] Raise ValueError if PNG sRGB chunk is truncated --- Tests/test_file_png.py | 4 +++- src/PIL/PngImagePlugin.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 2a40ab7be9d..1af0223eb7d 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -635,7 +635,9 @@ def test_padded_idat(self): assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") - @pytest.mark.parametrize("cid", (b"IHDR", b"pHYs", b"acTL", b"fcTL", b"fdAT")) + @pytest.mark.parametrize( + "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") + ) def test_truncated_chunks(self, cid): fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c0b3647884e..442c65e6f1b 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -509,6 +509,10 @@ def chunk_sRGB(self, pos, length): # 3 absolute colorimetric s = ImageFile._safe_read(self.fp, length) + if length < 1: + if ImageFile.LOAD_TRUNCATED_IMAGES: + return s + raise ValueError("Truncated sRGB chunk") self.im_info["srgb"] = s[0] return s From 208f87f9c122b0b2388883045f54198f07adb2ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Jul 2022 06:55:56 +1000 Subject: [PATCH 080/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4b313fcb9e2..e2ba97f21fc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Raise ValueError if PNG sRGB chunk is truncated #6431 + [radarhere] + - Handle missing Python executable in ImageShow on macOS #6416 [bryant1410, radarhere] From 6f4ff4edc49d10692922aaaf8a24f99d59c95c57 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Jul 2022 20:03:56 +1000 Subject: [PATCH 081/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e2ba97f21fc..bb975bb357b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Fixed PSDraw rectangle #6429 + [radarhere] + - Raise ValueError if PNG sRGB chunk is truncated #6431 [radarhere] From 2b9d48403a2c46d719e7174ddf530b313f487426 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 14 Jul 2022 07:08:25 +1000 Subject: [PATCH 082/242] Reordered SUPPORTED to match MASK_MODES --- src/PIL/BmpImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 5aacb10da8c..5e7c2be1b8b 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -172,10 +172,10 @@ def _bitmap(self, header=0, offset=0): SUPPORTED = { 32: [ (0xFF0000, 0xFF00, 0xFF, 0x0), - (0xFF0000, 0xFF00, 0xFF, 0xFF000000), + (0xFF000000, 0xFF0000, 0xFF00, 0x0), (0xFF, 0xFF00, 0xFF0000, 0xFF000000), + (0xFF0000, 0xFF00, 0xFF, 0xFF000000), (0x0, 0x0, 0x0, 0x0), - (0xFF000000, 0xFF0000, 0xFF00, 0x0), ], 24: [(0xFF0000, 0xFF00, 0xFF)], 16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)], From 9f6a3150924ffa3209a5ed45918315afd864a2a0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 14 Jul 2022 07:25:28 +1000 Subject: [PATCH 083/242] Added ABGR MASK_MODE --- Tests/images/rgb32bf-abgr.bmp | Bin 0 -> 32650 bytes Tests/test_file_bmp.py | 7 +++++++ src/PIL/BmpImagePlugin.py | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 Tests/images/rgb32bf-abgr.bmp diff --git a/Tests/images/rgb32bf-abgr.bmp b/Tests/images/rgb32bf-abgr.bmp new file mode 100644 index 0000000000000000000000000000000000000000..2443714cadcc65a7576d0b601e10810fd0a95d1b GIT binary patch literal 32650 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 literal 0 HcmV?d00001 diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 776b499e089..d58666b4476 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -129,6 +129,13 @@ def test_rgba_bitfields(): assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") + # This test image has been manually hexedited + # to change the bitfield compression in the header from XBGR to ABGR + with Image.open("Tests/images/rgb32bf-abgr.bmp") as im: + assert_image_equal_tofile( + im.convert("RGB"), "Tests/images/bmp/q/rgb32bf-xbgr.bmp" + ) + def test_rle8(): with Image.open("Tests/images/hopper_rle8.bmp") as im: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 5e7c2be1b8b..7bb73fc9388 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -173,6 +173,7 @@ def _bitmap(self, header=0, offset=0): 32: [ (0xFF0000, 0xFF00, 0xFF, 0x0), (0xFF000000, 0xFF0000, 0xFF00, 0x0), + (0xFF000000, 0xFF0000, 0xFF00, 0xFF), (0xFF, 0xFF00, 0xFF0000, 0xFF000000), (0xFF0000, 0xFF00, 0xFF, 0xFF000000), (0x0, 0x0, 0x0, 0x0), @@ -183,6 +184,7 @@ def _bitmap(self, header=0, offset=0): MASK_MODES = { (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", + (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", From e1d833bce58b4c43304280a4e271d9aa5dfc9c02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 14 Jul 2022 21:54:24 +1000 Subject: [PATCH 084/242] Install libxcb-shape0 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index c588af42ffa..16a056dd585 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -43,7 +43,7 @@ if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxkbcommon-x11-0 + sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi From 3f32b79303bcf62c27ba4df50585e56891fe38ae Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 15 Jul 2022 10:50:48 +0300 Subject: [PATCH 085/242] Replace version tables with RST csv-table --- docs/installation.rst | 38 +++++++++----------------------------- docs/newer-versions.csv | 8 ++++++++ docs/older-versions.csv | 5 +++++ 3 files changed, 22 insertions(+), 29 deletions(-) create mode 100644 docs/newer-versions.csv create mode 100644 docs/older-versions.csv diff --git a/docs/installation.rst b/docs/installation.rst index b4cee7b6900..f73c1f5a586 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -15,35 +15,15 @@ Python Support Pillow supports these Python versions. -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 | -+======================+=====+=====+=====+=====+=====+=====+=====+=====+ -| Pillow >= 9.0 | Yes | Yes | Yes | Yes | | | | | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 8.3.2 - 8.4 | Yes | Yes | Yes | Yes | Yes | | | | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 6.2.1 - 6.2.2 | | | Yes | Yes | Yes | Yes | | Yes | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 6.0 - 6.2.0 | | | | Yes | Yes | Yes | | Yes | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 5.2 - 5.4 | | | | Yes | Yes | Yes | Yes | Yes | -+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ - -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ -| Python | 3.6 | 3.5 | 3.4 | 3.3 | 3.2 | 2.7 | 2.6 | 2.5 | 2.4 | -+==================+=====+=====+=====+=====+=====+=====+=====+=====+=====+ -| Pillow 5.0 - 5.1 | Yes | Yes | Yes | | | Yes | | | | -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 4 | Yes | Yes | Yes | Yes | | Yes | | | | -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 2 - 3 | | Yes | Yes | Yes | Yes | Yes | Yes | | | -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow < 2 | | | | | | Yes | Yes | Yes | Yes | -+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +.. csv-table:: Newer versions + :file: newer-versions.csv + :header-rows: 1 + :widths: auto + +.. csv-table:: Older versions + :file: older-versions.csv + :header-rows: 1 + :widths: auto Basic Installation ------------------ diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv new file mode 100644 index 00000000000..b9630add1e0 --- /dev/null +++ b/docs/newer-versions.csv @@ -0,0 +1,8 @@ +Python,3.10,3.9,3.8,3.7,3.6,3.5,3.4,2.7 +Pillow >= 9.0,Yes,Yes,Yes,Yes,,,, +Pillow 8.3.2 - 8.4,Yes,Yes,Yes,Yes,Yes,,, +Pillow 8.0 - 8.3.1,,Yes,Yes,Yes,Yes,,, +Pillow 7.0 - 7.2,,,Yes,Yes,Yes,Yes,, +Pillow 6.2.1 - 6.2.2,,,Yes,Yes,Yes,Yes,,Yes +Pillow 6.0 - 6.2.0,,,,Yes,Yes,Yes,,Yes +Pillow 5.2 - 5.4,,,,Yes,Yes,Yes,Yes,Yes diff --git a/docs/older-versions.csv b/docs/older-versions.csv new file mode 100644 index 00000000000..f8c70758882 --- /dev/null +++ b/docs/older-versions.csv @@ -0,0 +1,5 @@ +Python,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4 +Pillow 5.0 - 5.1,Yes,Yes,Yes,,,Yes,,, +Pillow 4,Yes,Yes,Yes,Yes,,Yes,,, +Pillow 2 - 3,,Yes,Yes,Yes,Yes,Yes,Yes,, +Pillow < 2,,,,,,Yes,Yes,Yes,Yes \ No newline at end of file From 0f3ad23e1b1dc38a6af2fdb1c9c640b5ae106c71 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 15 Jul 2022 11:15:49 +0300 Subject: [PATCH 086/242] Add Python 3.11 for Pillow >= 9.3 --- docs/newer-versions.csv | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index b9630add1e0..c3f655e2fea 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,8 +1,9 @@ -Python,3.10,3.9,3.8,3.7,3.6,3.5,3.4,2.7 -Pillow >= 9.0,Yes,Yes,Yes,Yes,,,, -Pillow 8.3.2 - 8.4,Yes,Yes,Yes,Yes,Yes,,, -Pillow 8.0 - 8.3.1,,Yes,Yes,Yes,Yes,,, -Pillow 7.0 - 7.2,,,Yes,Yes,Yes,Yes,, -Pillow 6.2.1 - 6.2.2,,,Yes,Yes,Yes,Yes,,Yes -Pillow 6.0 - 6.2.0,,,,Yes,Yes,Yes,,Yes -Pillow 5.2 - 5.4,,,,Yes,Yes,Yes,Yes,Yes +Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5,3.4,2.7 +Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,,,, +Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,,, +Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,,, +Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,,, +Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes,, +Pillow 6.2.1 - 6.2.2,,,,Yes,Yes,Yes,Yes,,Yes +Pillow 6.0 - 6.2.0,,,,,Yes,Yes,Yes,,Yes +Pillow 5.2 - 5.4,,,,,Yes,Yes,Yes,Yes,Yes From 48ad0b1f381cabc2dacd932bf0272945fb815078 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 15 Jul 2022 11:25:33 +0300 Subject: [PATCH 087/242] Rebalance version tables --- docs/newer-versions.csv | 15 ++++++--------- docs/older-versions.csv | 13 ++++++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index c3f655e2fea..ed2369259d4 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,9 +1,6 @@ -Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5,3.4,2.7 -Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,,,, -Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,,, -Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,,, -Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,,, -Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes,, -Pillow 6.2.1 - 6.2.2,,,,Yes,Yes,Yes,Yes,,Yes -Pillow 6.0 - 6.2.0,,,,,Yes,Yes,Yes,,Yes -Pillow 5.2 - 5.4,,,,,Yes,Yes,Yes,Yes,Yes +Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,, +Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, +Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, +Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, +Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes diff --git a/docs/older-versions.csv b/docs/older-versions.csv index f8c70758882..6058f0524ad 100644 --- a/docs/older-versions.csv +++ b/docs/older-versions.csv @@ -1,5 +1,8 @@ -Python,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4 -Pillow 5.0 - 5.1,Yes,Yes,Yes,,,Yes,,, -Pillow 4,Yes,Yes,Yes,Yes,,Yes,,, -Pillow 2 - 3,,Yes,Yes,Yes,Yes,Yes,Yes,, -Pillow < 2,,,,,,Yes,Yes,Yes,Yes \ No newline at end of file +Python,3.8,3.7,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4 +Pillow 6.2.1 - 6.2.2,Yes,Yes,Yes,Yes,,,,Yes,,, +Pillow 6.0 - 6.2.0,,Yes,Yes,Yes,,,,Yes,,, +Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,, +Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,, +Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,, +Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,, +Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes \ No newline at end of file From 6801a255a3c7fd6b9e4abf1c3baa835b92fd78d5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 15 Jul 2022 13:42:36 +0300 Subject: [PATCH 088/242] Add support for Python 3.11 --- setup.cfg | 1 + setup.py | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 82873fce901..be3bc4b4f36 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Multimedia :: Graphics diff --git a/setup.py b/setup.py index 2701aa1ce12..71e853dcee9 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ def get_version(): ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 11): +if sys.platform == "win32" and sys.version_info >= (3, 12): import atexit atexit.register( diff --git a/tox.ini b/tox.ini index 09db058845a..21b5d4b506d 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] envlist = lint - py{37,38,39,310,py3} + py{37,38,39,310,311,py3} minversion = 1.9 [testenv] From 03df65e77b57148c2a8fdfe3466567d7faa7fac8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 15 Jul 2022 14:55:08 +0300 Subject: [PATCH 089/242] Docs: Allow setting Python interpreter via command line --- docs/Makefile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 309fc4458e0..f11d6b189e9 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,8 +2,9 @@ # # You can set these variables from the command line. +PYTHON = python3 SPHINXOPTS = -SPHINXBUILD = python3 -m sphinx.cmd.build +SPHINXBUILD = $(PYTHON) -m sphinx.cmd.build PAPER = BUILDDIR = _build @@ -42,8 +43,8 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - python3 -c "import sphinx" > /dev/null 2>&1 || python3 -m pip install sphinx - python3 -c "import furo" > /dev/null 2>&1 || python3 -m pip install furo + $(PYTHON) -c "import sphinx" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx + $(PYTHON) -c "import furo" > /dev/null 2>&1 || $(PYTHON) -m pip install furo html: $(MAKE) install-sphinx @@ -179,4 +180,4 @@ livehtml: html livereload $(BUILDDIR)/html -p 33233 serve: - cd $(BUILDDIR)/html; python3 -m http.server + cd $(BUILDDIR)/html; $(PYTHON) -m http.server From 55a501793401a466c369e4500eb76375a03425c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Jul 2022 17:22:45 +1000 Subject: [PATCH 090/242] Updated libwebp to 1.2.3 --- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/depends/install_webp.sh b/depends/install_webp.sh index a419a764609..ed17f2228fc 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.2.2 +archive=libwebp-1.2.3 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2e7c84ad0e2..bc5fb4d024b 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -157,9 +157,9 @@ def cmd_msbuild( # "bins": [r"libtiff\*.dll"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.2.tar.gz", - "filename": "libwebp-1.2.2.tar.gz", - "dir": "libwebp-1.2.2", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.3.tar.gz", + "filename": "libwebp-1.2.3.tar.gz", + "dir": "libwebp-1.2.3", "build": [ cmd_rmdir(r"output\release-static"), # clean cmd_nmake( From 45963b1b40ca2c7e66466812ce0842d917e425b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Jul 2022 17:25:29 +1000 Subject: [PATCH 091/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bb975bb357b..19d6fa994c1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Added ABGR BMP mask mode #6436 + [radarhere] + - Fixed PSDraw rectangle #6429 [radarhere] From dfa6655bdd511cda37e568aece8b185cfc5c2d6d Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sat, 16 Jul 2022 19:04:42 +1000 Subject: [PATCH 092/242] docs: fix simple typo, unpredicatable -> unpredictable There is a small typo in src/PIL/Image.py. Should read `unpredictable` rather than `unpredicatable`. Signed-off-by: Tim Gates --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ce32c39ced6..6abb1249166 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -646,7 +646,7 @@ def __repr__(self): def _repr_pretty_(self, p, cycle): """IPython plain text display support""" - # Same as __repr__ but without unpredicatable id(self), + # Same as __repr__ but without unpredictable id(self), # to keep Jupyter notebook `text/plain` output stable. p.text( "<%s.%s image mode=%s size=%dx%d>" From 2944ff18d6ac79b881e3a3ec8d656fe08a41d1a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Jul 2022 20:02:58 +1000 Subject: [PATCH 093/242] Support saving multiple MPO frames --- Tests/test_file_mpo.py | 41 +++++++++++++++++--- docs/handbook/image-file-formats.rst | 11 ++++++ src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MpoImagePlugin.py | 57 ++++++++++++++++++++++++++-- 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index d093f26ccd8..849857d31d6 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -5,15 +5,19 @@ from PIL import Image -from .helper import assert_image_similar, is_pypy, skip_unless_feature +from .helper import ( + assert_image_equal, + assert_image_similar, + is_pypy, + skip_unless_feature, +) test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] pytestmark = skip_unless_feature("jpg") -def frame_roundtrip(im, **options): - # Note that for now, there is no MPO saving functionality +def roundtrip(im, **options): out = BytesIO() im.save(out, "MPO", **options) test_bytes = out.tell() @@ -237,13 +241,38 @@ def test_image_grab(): def test_save(): - # Note that only individual frames can be saved at present for test_file in test_files: with Image.open(test_file) as im: assert im.tell() == 0 - jpg0 = frame_roundtrip(im) + jpg0 = roundtrip(im) assert_image_similar(im, jpg0, 30) im.seek(1) assert im.tell() == 1 - jpg1 = frame_roundtrip(im) + jpg1 = roundtrip(im) assert_image_similar(im, jpg1, 30) + + +def test_save_all(): + for test_file in test_files: + with Image.open(test_file) as im: + im_reloaded = roundtrip(im, save_all=True) + + im.seek(0) + assert_image_similar(im, im_reloaded, 30) + + im.seek(1) + im_reloaded.seek(1) + assert_image_similar(im, im_reloaded, 30) + + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) + + assert_image_equal(im, im_reloaded) + + im_reloaded.seek(1) + assert_image_similar(im2, im_reloaded, 1) + + # Test that a single frame image will not be saved as an MPO + jpg = roundtrip(im, save_all=True) + assert "mp" not in jpg.info diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 30452c4a6c2..1728c8e0579 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1209,6 +1209,17 @@ image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL methods may be used to read other pictures from the file. The pictures are zero-indexed and random access is supported. +When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default +only the first frame of a multiframe image will be saved. If the ``save_all`` +argument is present and true, then all frames will be saved, and the following +option will also be available. + +**append_images** + A list of images to append as additional pictures. Each of the + images in the list can be single or multiframe images. + + .. versionadded:: 9.3.0 + PCD ^^^ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 4efe6281ad6..a6ed223bc6f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -711,7 +711,7 @@ def validate_qtables(qtables): qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) - extra = b"" + extra = info.get("extra", b"") icc_profile = info.get("icc_profile") if icc_profile: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 27c30958c9c..5bfd8efc1a6 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -18,16 +18,66 @@ # See the README file for information on usage and redistribution. # -from . import Image, ImageFile, JpegImagePlugin +import itertools +import os +import struct + +from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin from ._binary import i16be as i16 +from ._binary import o32le # def _accept(prefix): # return JpegImagePlugin._accept(prefix) def _save(im, fp, filename): - # Note that we can only save the current frame at present - return JpegImagePlugin._save(im, fp, filename) + JpegImagePlugin._save(im, fp, filename) + + +def _save_all(im, fp, filename): + append_images = im.encoderinfo.get("append_images", []) + if not append_images: + try: + animated = im.is_animated + except AttributeError: + animated = False + if not animated: + _save(im, fp, filename) + return + + offsets = [] + for imSequence in itertools.chain([im], append_images): + for im_frame in ImageSequence.Iterator(imSequence): + if not offsets: + # APP2 marker + im.encoderinfo["extra"] = ( + b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70 + ) + JpegImagePlugin._save(im_frame, fp, filename) + offsets.append(fp.tell()) + else: + im_frame.save(fp, "JPEG") + offsets.append(fp.tell() - offsets[-1]) + + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[0xB001] = len(offsets) + + mpentries = b"" + data_offset = 0 + for i, size in enumerate(offsets): + if i == 0: + mptype = 0x030000 # Baseline MP Primary Image + else: + mptype = 0x000000 # Undefined + mpentries += struct.pack(" Date: Mon, 18 Jul 2022 16:16:06 +1000 Subject: [PATCH 094/242] Do not quote Pillow version for setuptools >= 60 --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 71e853dcee9..37477216d3f 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,9 @@ import sys import warnings -from setuptools import Extension, setup +from setuptools import Extension +from setuptools import __version__ as setuptools_version +from setuptools import setup from setuptools.command.build_ext import build_ext @@ -850,6 +852,7 @@ def build_extensions(self): sys.platform == "win32" and sys.version_info < (3, 9) and not (PLATFORM_PYPY or PLATFORM_MINGW) + and int(setuptools_version.split('.')[0]) < 60 ): defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) else: From aba0859db9261e94feaa592615b9d70cbd2b99ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Jul 2022 06:17:07 +0000 Subject: [PATCH 095/242] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 37477216d3f..a2b2c691098 100755 --- a/setup.py +++ b/setup.py @@ -852,7 +852,7 @@ def build_extensions(self): sys.platform == "win32" and sys.version_info < (3, 9) and not (PLATFORM_PYPY or PLATFORM_MINGW) - and int(setuptools_version.split('.')[0]) < 60 + and int(setuptools_version.split(".")[0]) < 60 ): defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) else: From 77402067fbe06dd9224422c6a39a738d6bef66db Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 18 Jul 2022 15:30:00 +0300 Subject: [PATCH 096/242] Omit ":widths: auto" Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/installation.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index f73c1f5a586..9e9dc52b3eb 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -18,12 +18,10 @@ Pillow supports these Python versions. .. csv-table:: Newer versions :file: newer-versions.csv :header-rows: 1 - :widths: auto .. csv-table:: Older versions :file: older-versions.csv :header-rows: 1 - :widths: auto Basic Installation ------------------ From 3a7e29306a1c109a2e5cb5b6dabcba9f6e0eb9fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Jul 2022 07:40:25 +1000 Subject: [PATCH 097/242] Added release notes --- docs/releasenotes/9.3.0.rst | 59 +++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 60 insertions(+) create mode 100644 docs/releasenotes/9.3.0.rst diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst new file mode 100644 index 00000000000..da045a50a98 --- /dev/null +++ b/docs/releasenotes/9.3.0.rst @@ -0,0 +1,59 @@ +9.3.0 +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Saving multiple MPO frames +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of +an image's frames will be saved to file:: + + from PIL import Image + im = Image.open("frozenpond.mpo") + im.save(out, save_all=True) + +Additional images can also be appended when saving, by combining the +``save_all`` argument with the ``append_images`` argument:: + + im.save(out, save_all=True, append_images=[im1, im2, ...]) + + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 597c804f861..8c436be3bd8 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.3.0 9.2.0 9.1.1 9.1.0 From 13acf0a545da94a77804832a5da956d1a713862e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Jul 2022 07:50:04 +1000 Subject: [PATCH 098/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 19d6fa994c1..d3c96fefd39 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Do not double quote Pillow version for setuptools >= 60 #6450 + [radarhere] + - Added ABGR BMP mask mode #6436 [radarhere] From 37e794245ea770662f54250a383e206ee0efaf15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Jul 2022 13:11:17 +1000 Subject: [PATCH 099/242] Updated libimagequant to 4.0.1 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 31fc2adaabe..9b3088b9450 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.0 +archive=libimagequant-4.0.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index 9e9dc52b3eb..f147fa6a732 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -166,7 +166,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0** + * Pillow has been tested with libimagequant **2.6-4.0.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 0844fb0ed33b01656d4cf6ff0c9cd63342611476 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Jul 2022 09:05:14 +1000 Subject: [PATCH 100/242] Do not clear tile if not updating the image when seeking --- Tests/images/comment_after_only_frame.gif | Bin 0 -> 54 bytes Tests/test_file_gif.py | 5 +++++ src/PIL/GifImagePlugin.py | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 Tests/images/comment_after_only_frame.gif diff --git a/Tests/images/comment_after_only_frame.gif b/Tests/images/comment_after_only_frame.gif new file mode 100644 index 0000000000000000000000000000000000000000..8188b68473246080bb04e13f3a7b412aab4de3bf GIT binary patch literal 54 tcmZ?wbh9u|WMp7uXk Date: Fri, 22 Jul 2022 07:59:30 +1000 Subject: [PATCH 101/242] Moved code into separate function --- src/PIL/ImageFile.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 99b77a37f5f..9f08493c13f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -499,9 +499,14 @@ def _save(im, fp, tile, bufsize=0): try: fh = fp.fileno() fp.flush() - exc = None - except (AttributeError, io.UnsupportedOperation) as e: - exc = e + _encode_tile(im, fp, tile, bufsize, fh) + except (AttributeError, io.UnsupportedOperation) as exc: + _encode_tile(im, fp, tile, bufsize, None, exc) + if hasattr(fp, "flush"): + fp.flush() + + +def _encode_tile(im, fp, tile, bufsize, fh, exc=None): for e, b, o, a in tile: if o > 0: fp.seek(o) @@ -526,8 +531,6 @@ def _save(im, fp, tile, bufsize=0): raise OSError(f"encoder error {s} when writing image file") from exc finally: encoder.cleanup() - if hasattr(fp, "flush"): - fp.flush() def _safe_read(fp, size): From ad2c6a20fe874958d8d9adecbbfeb81856155f05 Mon Sep 17 00:00:00 2001 From: REDxEYE Date: Sat, 23 Jul 2022 00:30:27 +0300 Subject: [PATCH 102/242] Add support for ATI1/2(BC4/BC5) DDS files This commit adds support for loading DDS with ATI1 and ATI2 fourcc pixel format --- Tests/images/ati2.dds | Bin 0 -> 22000 bytes Tests/images/ati2.png | Bin 0 -> 28408 bytes Tests/test_file_dds.py | 14 ++++++++++++++ src/PIL/DdsImagePlugin.py | 9 ++++++++- 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Tests/images/ati2.dds create mode 100644 Tests/images/ati2.png diff --git a/Tests/images/ati2.dds b/Tests/images/ati2.dds new file mode 100644 index 0000000000000000000000000000000000000000..7199dcad7620d3b6e3e839370892f6ee901082fa GIT binary patch literal 22000 zcma%?X*d*I`2R&4MM4tlsVt=^3Wa2S$|Sq&`&edYc4G{ZZ7{>km>FgaV>g(=V1$Y! z+O!~HNJS*H(}T*d-}Qg@d-Z>D-}iN$>zsGzI@kBUzn|mi=yfzxKhb#MRre72wvi zC>33p+);i$4(_iq`KYiOxhX}zOuWx0s`qa=$1M~gfV(EJ!552Aw|vEVIqIyK@24k< zt!J6wdL#Hq8lw(uF%PMl#WjMGCkKjcw+x^J!_%?W#w`ZIK1xKcB-i@+CBWhI{Pu`@ z@$CRYOGDJ`sC^@dUJI9X%FTv~nek}h=z51bD=8^tL%pwygU2nuYNbr%B%B-W1dXq! zC%r4SvwH4HPC#5OyCd~TC;ihebM8xd2Jv8bd1Q=S8mUWJ@vg#38p-!k)>;2pHeoKf z%axdx17zriDQ_*|M!_DkV9q!^)qNUan+ptm4Wtk{ z`@k}whQ%a;NJcNIhx5Q%79g6b0*|ozxqlw|gPr&+SPE6rFCpNUe>%nfq@e4k{nh$DaRzdbGf{sn})(2L3Z3H?;2>tzby$(!l zt9MBXr1m(v6voS*%YVCjun;3TIPptyf{c^i6db*^kdYWYaAs;(KsHFWO|h7zP@lZU z*c7?IXrv@a%_(>V1GnnJQdI=!Af*>yj&vTOCjJgZA%fnp;))N7R8PFgjk{6349g{# z#Ma;vVztC&sMD<0WiNVpWaJ%9qZgeOF`t~y9Q5$2cOQFZazivIiD|a_o#<&V@@&NH z>A)qIhR(Vhu0~St2g_Q03$!usNiu;d7y+{RmtC%DMO+7TIMFkZkZv;`bnE$YvmzEF ztoH%>)Pd0w!hVgl&p%dc&~P7d$Ld&K5_zM)ic-x?a@l?H*6*$yqUe4}jqlbRQr@_V z-i!W10`{}{54#L1S}KA6aNChbD2Tm12)f6|Hf*d4ZqOSEqnnoaZ#Kp8p{niVlTRwc zlW31h9=O+ntK4?zUjf%fCZ0O>CUl}G(op^t;sU=AlM&~UsRk^C?HTAiWYIw9s2DvQ8}(E02#Kz1NyQ0As9@ds1{tHS7Kf9~o|=khirgkU2R=cS zvH?2^w;VFYaAFMfA}}?3N+3J$;%2X87r|x^ch)C&HRbmIqqh}6z|#{M5I2IostLI~(s6mr?b~K& ztnTNBCBt(xAI3XkoBPumgf{D;NTa39H^yuWR~?@hWnk)}Wxlf@R`YS`8R7$*aHB0{d9E!tVRo0hW)4fdUU8rX#TD(u>Q1pN;i)W z(zn~dYXSL@Isnb6yG`C5QRA&g&)HS`@v@rFTje*Pqj;;p9WP3Qmpwv%%j+@xPujVt z4pegEJ@=e#QQs%TMjrdo^?4u{Kj%|cmPKpD)g5f_SUg5YN)2`^=QeRrz?RY5U^xoW zZ_4_v(328tQiGIvKud#a?nD%xHp+vjHL0u&TGpc9R8tBzJL-^T-PMPF(AY6;KNAyA zY2J)o4Su>#dUY#x9Cq^u&AmGgjCVLP)7^g|Vvi)w0{uAgcJNS{J)#60Slfz0{Hh3$ zDsS97nN<|8-4qVATBm_zzTA4|%*v0GrVeTXaEw^uQ?b-ZzL;2vJS_lDP-2bL$R|#M ztD@e&xFiI#7_p~Eq8mfc@saP25{=sv8{^uJhe`aanXtES@Y^qeDj|_zVVLy4a=#{k z@7FgfeZWZTm)+l|JK`$jo^=|YD04G&SZ}j{u`jwe#Zt~!Fg%Z_y-+c*<-xQa0jKAd z`N=5WDDi?k5B1vR{A@r^X$0npKz3(b9^pbT`$#4s0|I+Y>lty%M)^5%3@zxH81<;3 zN*h)tv9)>OADCeZ@`(ZSwB8mD%%MNwwls~26M?dlJY|BP10MJ-P{TDSAvY>$plUZGZod!Kty5YknOJvAlLpS#X? z)N+vEI^^#}zx+0SnR!wL4<^>Z{t#g6vySIG?l=L}%)HNC2lQH$Wy`JPpnWov=l zgIi^2+>IZdgR6W@bEz~SP|C&(Oj(`^=FnpWk;V;EbT;^7lc$j&zZBi8I=?nGnuFJR zVg;&)*GB%FwOk36)Zv=H5v5KdjV6TI^$$1}B>b|kpF2sCoZ?h&Y*i%!!ZcklUDA1(~+e2F?<;Zxj) zk`9NLzynTCr{1JO#2Lw(Kc@YzBo{7WkB-%lFU!Zcb1o=!>H z4ya;?z5eTt}I($b(T#ip%W%Qw15#%VF?W(DVgJjS-V7Wy<0-Mu` zGDxJu-+J9ll7DYI|0l64h)(HvGvN6s2RlPG;1`v6WDKNBPdyY}H1uEX-3?&I+sEVw z1)H%znnit^F7#ZWM$U^sH;eovw~}}yNLB>6-wR z{aI))ig}%)B|VWaFKyq&DGFGMr;e}M)r36nhX(76 zUW*$inL_OAF=JxkfgF3`(INs~*zdPN&csfbGu^ktOOrM?C*js#3Xq8x=6?bF7I_ypef|E~ zrUCZWB%?NGzR06<!4b8tY7KS zC9}qxK>fGK$%Xq35lJPbKa81d&@v`aahIDA_SJcXbA?`pAsG++czl|PTJb2w85QJ( zyKYgJ6f$avrWqaZ<*`a1?d}Ctt9-UsZw9#5-@FM=m`ijT8>x&KS~}lx)?FBV6#D&Z zA+X}w*Gn#PjkIEPuvIIg2*roMti-2x3A2(ww&^^^a2hGKGF?BIpPqQVR%q;hBAxiO z6{WpXgNa!&%S^0%U6io8@GAAcV}j`Ty6DA+opt_CE#SS6D0#TW;Uy6LRUsJMC5n|uHTMv>RUz}8 z^iS$HYS8=paA+5s`e;2D@egfsQEblx%4wZXw<4%3zURQx)iIS#*AOvQ6genMsaSaOaCPosvksrdX+^cn#RTt@bqdjP`qXEUcQ1`yb zt_JdY6<6l_q%>lAnj_O8F2Kh6~ImF0nHDsod8g4gMg5nex(GSzctM-d9IZb#4uMR zrzj4P;0}Lqca_A54(UU+h&JJ+YbT!`Zd_^d_|5wT*ez4-XY_F$8SmKwvv~`}$+p!J z4p`mYdf{a&n(s2-u{@TQq$h2IUH48W9ybdUq=C~(`{#SN$bQR6mS5EWee9k%&@XqY zSW0j7U9rlgFFoZ(!}yoZ20CYg4f$VL<-fV!YP-s0c0|?!Mfx0!LH*vf3M*Ctxllu95eW}{r!tI*Wn z&(~P@_|B89v>*oscC^>&{GV#T0`QyRp_1K5Hu2Zqn1^til<t6FJi0sg6bX&ZDS8$)hZnHn1{fd4ZB z!}VBJLgyxA8}L>=mw8&BHcU0|xXHevoV;D#7iS_h|K9jV449~G>i|oO5qr^tnIq&v z?AQs*=*`(OQ`=f(y{@YOzg<1W?c1-02(xD4iM;+6;8JvEN1x}T;IN|?qin?8$?&te z1s{fMV(vQk-KZDx;wlw3SdOeJ;?UKhx8Ob|aAcHv<#+=XVN`wTgc*t(dpqo$aH=ye zvFi8{Az+$~gDg!aFR~agEh|>;t85k&<_|e2)6Y(VKK)Vn%!HSiF{Asjv^_2MxkT2g zwJs-7@=xlL8Lm7e(!~;1rrrpZ_|rvV^(Ii}S&Q$zWu4wFi;}HIQch?oZqHL?w?d%4 z>A6E(-!fd7j^z*FdX7;$3@TQ5k$i@jXRS9pr3ayWmyNicPr z5lAxyZ9o|%2~OD^!0GuKwCuA#WFmu-9HupN06LaOXzuQa-1Ra8K{p4T=-kRqPAZxI z`DTkSeslAffO)Shxg*$D>QI=N!x2-sJzKI8rKfhf#=pvry4gE4dv=~hz{?HTtexwH zygI;q8woDD47DFp#rXnmy&VNl*(%grDbXm^)Z>>&{|U58gMSrV`D7H6GPb1#+WlKP z*sombtMxBkTMo(avAwYh%;AZn?3cxarT+w>W04?QC%Mc|rdl2e-)zF_K6}Zq?xThU z$it1;jGUw!(VUZm)nvSZ-!x*JnwB)xS5z^+H8auhmBMQrA_FN^5Xzh|ViDS$H=@_n z8iNi+23j*)y($K}N)k)!W;>EPLZSMs*}8Dfr3%wF!;0pcdYbahlap>G9zXzg3oEZY@E;BMd(0k&v!pb|BPraU~C`0Yg#Ov<7{`}_ukw~-Z5X3{-( zMzX{{dco$nhe|B3v{BEFNML!yBnzQJrqw~I;JkOom5ThNo%ibvuL@jGja@Y*_>>2= zu$kS>n1FLut9xW z)(DOaOj-#pc#KiyBnFYzTf$;jRi8-EZGNA2KEGqt-%5B^u=~&QJS%G7s4D3Lix&-o zywtGqt%N@xep#`-p(L>nwum-*&kS>Ynx_$kV#V7&n!erDSsZm>j0OWpG7~q9)%n*< z$Vn;1#i!dl%X|zUvPc8=MM$FltuOK^~-iqH{dG*Wr+{!b{4{nwlIptk`r;9NBce^Ms`N>N1 zWmf^@;M$LFqtY^W$#UA@PLGmnQ=`rB{R$<~(b7G^=i15u>VZa#(d(rl-sxnWlQs_$ zOh2SruO&W3YEQQtT%$K%%zGPPX6M=)xa8k5l-lT9OO}!l^chbz_z#>PuG;!u=GYcCIAq~ScBXa}Flwo(V>_ob@!sd(71qD< ziS{nTVZ~z^NpEsb>!ClUkxV?=XZ%w#p!fP|r17oHl#U{WjvrhqWU0&3)RywM9Vp8K;jg4CX2)waI{@#cz~8gxF3 zBtd$$MZC;Rl-YT3xAawIWXMz}eBQP=ZtlG6LCqAV)Ob{t7) zd@p8#Up|>D1WH-g?X#>_Hpj>?M`B(A>QB_P%4W=6P6riG}}*DvH{_ z1Jz@xUyoXXxNr2h7b6d64sL@VVIj9Lv_Wnf$1bd)u@WMllgMH7AkO0xgi4U)Z# z7(vC$-M@$<8K}1}tA=U-(xk2PiWpf*7^ddP+|>zzBC8DoAHne;q?+Lbdq0Y8l9)d^OtYJ{-iOWLH=|{g-RMkI~Z~GNNs7L-m8#pR>lP$m3;ZI z2Am+==|p$y0;?&`-g>?H*`Fq&xPAEIyVLwI<+~kEKgS8ej_A{a9 z-PCoejX*{}*VuD%pqZ11|1){U!+1$P!{4zBNa#}L9RfZa`(?dbG z_h*7u_9H6za6SYs9YX+*l!wmxD3#wfYK=w&PgEW3;RkSA<%K`3i~N@TFp(qJ+N&zt z5Eu4~>+#2eB?qm(*QQq;jqTAL&*mOc9dHSFIa0kp4=9He!u8GE)YIzqlf3<&5u1u9op6&mCc%%?3QL+nckYhO$8THkVF z2p+7S^Ym)0%Yl3N#p(QTTOF_E5f(LJxBxI~`78~0#2V4~C4w5E_1L7q5=KYr-TylD zp-JLD;&NUaP8S6cgOF-YB2Lsnd6BZvtQ4%5!}`ke%qH!#&fW%1<0ZF$pI_UBD?}T8 zUj5@Mmq*ZrddWI|ZwoQ_Ko>sjWX5RsX<)CR>Ox)*RAFB2t_Jx9D#-@SSK+VS8?Gfy zvq8L_5G^)>7so^2_D5<~pb=n7j4=PGiiVV zI(PUYDp0;MKm7e)1Fb&zrmMY;N#tX3u@~Y2Slbv?>zApH{5guK2|tYYf&jX!A!;>X z)k1PHZYW`$U%J7FGQ6w~G^VG=(e;(Mx~Apm2nG7o#Qhpr9=K9>`ej~Hm*OkPXG1o6 zB~%*q(2fH(TohRpr4+>q0})!S77XI;wMS>4=+H1XK2qMW#N5~;c$8o-uAJodi*ESe zW@`{=k8yxiNg1^5bEU~#W4X_@Kx%d^ztJysl`L^!G$6FBDvJ_|st{MAnhr((q@$BQ zyoT-?Ex-%&{;bAM=O}ERc7wL&qG1SJ*)1nH-IHk=z}YE z9Eg_R^l_`X`hbd>0B^t-p0&0q{ks>U!s%>7$|*Lp8shp(A?i_LYhXzr)lAKv=i6g$ z)MM#V3&VA{MrF@d_~<_DbluThmZHsjx;OR*71xV+?YprQ$H9j0b@s1sM{VY#o63>)0i1<(NhLU~!2}=J z#lv&GE+Amt6c{GDTw6lP$HP5_^==1|4q?mma-F?-X7ctKQ{muzE_i8f@6YF#3lU2_*yXwSI_!b^=K9?e zHE!RhnsqGXDgNh1uGGXKY9Mb%n;fk-8t^svo$_yNdEiI8Uf_fF)Sy1Te{tF7e1k#I zpjbE4h2_+u?7BA)!-ed9Tl4OSOEu~Bm53tPnOb<1g9KI6qr@YpC1VPd zECSOz)w=LePJF`}RcbDu6~%ovSl^JEjXvrCJ8C>Af(i;!V_vHCpx1uycH>(1L>51)Oi-QP7ioM2 ziY}$5_}Oq$)Mu7JkWCi;>rN{0z;sdML3^s<0<|y+yRa1OvZc}=?#+e!s^p;=93#Jz z@P=q@x2AicRn39j!CV79+tT=;p3acp4K#d}d#BvsN1k&|^Zq5id{YcxI#CInFS&6c z!N)*#Qy6ga#dODV7Z*j#mel*NXGXea%8uwN79b5BzRe{lbI}#R_8Zk6JlFur7;%{` zLOxrx!-40Sa9TjVTt_nv`@FRk21jQl_P1-uV-vCymeuHs(&8FuTd);&!mj`{X|ABH zYf>8#)m`-_c&!=*eVMnm>%XFK)yppG1q!7}x_j~^2iLO*ACIbk)BTi@jG8*he5l1F zn4ve4rgmjSrAp-UWdV5+72PHy>y#V72t|{E<6CO6wKko@glCX;1T^=<~@78=W*U>k9tpI8%1 z_|$M+AH*qqbTI~MAsfsq=0sOJnrO&3iQIJB=?dlgj~I}{Gv444a|}wzic3E zexD?*s1*~c#$M^EW()lGS$*1SZNl}cbl^n13a^U(M~ikSKDCOl{QK+cype3Yh5?Ri zc0ZS}JU5|hr^Sekl+u4!-euw>Q`f8OocMkXJt3HJaTUnSL!t8Xu_o;HvuBoqev86> z4Tl#*KP>?XFj{*5!izz3BhB`}u58>1XO;9<7E}VlT7+L!V&kxjL&=xq_#{ArRPT|r z1CRVu^K@Y=iAy{kBxJ~}Kwh=QH(I+4#f}l*M+)XOBN1DXka&wnD_Tk z4D1K1y~~_C^|8uhFM%)SOQ7fWx!$*>FyK3OJa^0c@L@$d9c8~% zM*B);H|%WPkXE{fM&^o34x!sTd8tTustJ?(#8U$_r#<7 zVIft>q_vlgn)5lh&1?xLr{f0d@U}}@Iq+lU`I)qPT@?AZS-JCk4=-ZYxLMn4 zKRdL~DP>`T&++a#4;S#s%@G&bb-Z(Xh0uM^swqAzylaHFw+5}d#hx?XEcn8$I%3++ zFg7PagxK{jT`iwo86fU%R6Ot6?UVZ8h4R3ymO$SFYcsp<2<=p-q?E_-@{|C#$;OuX z8!*hc{4--&7RgE4-nG1%P6UZvG><&UO4wLlI^_z?Ozb=SBfqqVj&nQ_ZMHBsOXU^Oy33PEh zPjQC|C05m$+FG$*i;LDWKLUTkbWwSDe|b`+4l*g9c8hRE;(_}ky6e}0#h5LQmF->ai!B|3U>J|~!F;S_zdA~KoTcuk9^-)*-SB_)@< zG740x{)-vJnT^v6(6hbFNCkcg{=(Zk@j*6B?6-~*zpHEM#N6#$)fsJtHwM3RnxW-n zlKXC|&hDl>+=#w#Ol_z**=_Og9pE$ty*OM{-qV(YJGdVc=#R=uCQ(@r_Zjm+Pk-&c zIC7)iC(_y6$X}J`bMFCaa|H6&H!7m4keN&j<$38`U}qu5>eRTOQ1U!mK#I}a9T*?RCkxBN91y61=QZ96G@!TB{khR%i151jhWurtTMiZpY%&O zxi(XijiesO=%j-)k__*v9EU0(1P!^B$p2Q`2%lT793t$g_IZm@I`-bRE$t`c zqHtF&HMzLL>Pwy88vk_>XJ&_Q z!{HhKm}9A35Rp&zsqT#4xC+HTiw?&mC#iufSz|lLdQCDVK#<;rj9r^c{dK+uU-rOG z?ms~h+B|0>Nn1k3$WdNuY zRMAsPfoCFbSdtSWh+QV~L*)2|R+Lxu?TiGSxgBOZYRQ;jd-2)keQ61`dyWk0Qy9>r z=br)xCApZHMJ*$Ll~Sl^f2@yHJ_RAWoq~#d!;65X3rAn(-z6=3h?2OSe&4eajW&t0BSAkaU#S3JHG>Nz-$~#Y}6c z3M>AUM@8^=xbJ9db7yRAfUQ$&!fszMvAy@A27o!yt*V>FPB77Mi~g6Lj$7G2@lhFE zh%ypBcUm?tiPk&N&hKC~gZ}(iw^pLc4{g1N)z()Lgn=ykeZi{DXp3FULjX%QWb1nq ztqM95gSg8vE5%iM9XAWuGV`=G#%L>{G(SrMU)!Lv{0an#`{cfC%*Peq_%=O-D%DNsq(92b?xXF(+)9gzNa{GHMITx`PqU<;BD5jMnrx5on_0( zLAwglF4)D!d+SsjJ>t#DT?jfM%(=^DExrH^UZd+-ZOK8ZSPAuT_Jt`p`e?cWD4Q_u z*{Gw4%*UPb>(&u`&5D=SHIJP?lAa*4*!A+>ooslzqo7zLf{Vxfl190NNkZB}^UR|W zBDYzI*<`;`<+T}*r@$S;0Z#lU8qG-HfUu#N_y?*q#8Uo1xz5L%(YgzQ$up;G?M%ld z7kzD6u|@qZrllRHXvA9U;GVYx=@AM1NF6N>$}Oa|#?#2MYhU6~AJ;#_)JCMML! zC}C`OvPW!H3=;5G?o)c{B~^om8agX?>{obWZ6|GMK3ajRM`Glg34-9KNeX6nadHmS z*{9!%!?i=Q(?8_b`D5X?IpnWwx0K^_gWE-%@O^D6+3s74oC;WC9eGMwg45)&q)&ZW zQCrPwmM+ohNrylP6Yf$P`j8_dd=Q&OIFqJy2v5ySZal2#>w?OH1>{5pn0YWr@@oILmLA+tIxd7`HG_)~2Xx_F zikA%_^;A2)@5;h|x0rC&-hINNRqmBP1@v21O^qR%In6=b&h)F&tupWPBeG*OjLcGN zhS(q@14X@qUJ}33C$x_v@FoaGyCPUFq%`Q_-ek%Brt-f!jwUaU;w1P{Rv;}FIVsdl zNmynXElTg-{TE)y&{1m7Bkp-xci@n}U&D4!7e)6T_eH#HDvZ~07V8{+%0Mxiu*FZ% zd?ftYD0WS=Bn1w?v9>lS2r(%VN;~he!h8FV!E6c(-MHS6vOd{Dh?(+4wI!k|>GQNz z_pf{G&g|OK>RFDTR`Q`HB z?mg*7aJ#bND~@gtlb@qwKlPlEJakD5;cv?{-QkdjvSxm@bx5fIr#ZPaM+Y`}J#nPy zx^rtHF~hK@+Zi`K=r_rQuL`SONHDh0Ycm6?W-I^5G_ET; zK&~r<_@^4BT}R%(vV|M6?ajUe$KYHzI2dPZ)lSA3cD$5vFr>zB?n#IQd@F%PLu1cb z-lk!ex0Y2DB{Gw65u2-&nvCScf6rKF*4-f5DjYt%T{bI$^lnufv`QuJ7!x%Fw~$f# zbBXVr$c(s(i$@!#{_5Rj8`;@@VN+1jk5cm(1T#RbjRLfnZGnq-KTz%}etNaunl;~t zVmr6>sX3dGC4qnZP+_uhbrFhPEi1UxHn-C`$dU7`9H_)=h)1auXWc+E#`~6m>iHyy4Vap#TJd%VuydYskB3^8N_qdr0)U(Ae zTN{c(xJNr9EZW=Sl$_-aH<-1+1xyUq7gY!Ky4Et8HG2!aG89o7Jya5){`AD2aVj;r|6xt;lM{JR6EIcPKp_had>l2o z#W|mF@Oh?Q_t$!0dHr16lDNf;PZrD`NP?D)ZW)ga%t#2n52u3M|+Gg0pXzVuyr zkV#zH5~uU3xguiO&tv8Ykp-el88*hb?Y?6IHuXKR3%LMv`M5D(1vdFSQ|#x_gb-Eh zpZ(e=Ox*EXhqVsN8#4pnGp=@KQ~(T7J&5u_{x@W ziI4dT)F3`0D`MENP?zf2hw!0@+!i6ni$wxc|{2L=GT?Zhd-#L zy=;Zo06#cSh(e`Bs1x->qH$?1swzkp5o1#x_fp}{lfgDlbi=`{Lj4zAus?9Ca|ahG zt{TTYWbesW`j42ZBQ)6bFdZGawzI$zSZgxfU0aHxP}V|(fAwDE8}9Du z)l`BiO4VeLXIPHB?-O69Xqn@r;MO!8KRshmWNd_%{8d@%|!b8!W%RY*(*;V2!&)YZvHdxMF9-{CgjsX3>$OMLxNkpSn0W|Nx6JGy%8w1 zWjAanx0|Ny8IyB{_oqJjY_4{RD)N5uY4CC&jhRwbHw;p;FCk9sOnLpNH6IrFn4(1P zrX)QM9y-mUW&pKxU~)3@dDz`yEYXI5jQp{n=B1Za9&PZc1k)jD$L77+*!(`t10i3j zm=4}yM21y#Qk%)$gl|6pFF(%TB;7YZ=EiKU3V*-CvhxS^261)b>Cs~T)$mvBCIPO@ zrmenExudxarMVYnGattbO1-`J&lkVqYs4Dy8$VZJ^fL-U>(XZ7^|7Gmq0fx7(JA^$ z2Mu{KPFq2rJuB;1)@`SdLdVd#vS4NlEY z#Lj#JeICq8x_@&l&<2%@YYVKI0k`s!e*T^;*SX4%B(?2;M-eJu6pxw_p(`7A;api+ zZf9fk^D8*Rd0l3VAR2UIlw1hgUL$rZ{_7j`D!6Aa)*AJZs8%~a!L&@-xFZn z4C3xRvSb8k3EJ`%@3UhW#wr#35*7;U7^@#CQ<|u~%K|+lEs1axbkyI@D8@=ZkAJpB zgzyziB)Wn0nouMEh^0vLVle&jOOWtTNkUID8>iL9fCa-DC3=p0&tlW-0YyU`_|g@s zUZoQS!g0L1!x)i;!JDGX>#nE618z`yOh>D{Y%l%_dOTEOUvVx>wGYu6lfD>bKpf_S zOt0C0EcmbHhSBs&D(O56=@r<{C*sR7P#2CvKxclmZ2zsaF+(E4gQORZT!pH8bnc5Kx12iz%`u9hwh_yB*UM&}y(SQ3{-2(5L1j6^j zHpDIl;!|w>i5)xhed>ezKX+_nLv`LGO~`X?!9f{C?{WbJK;!r07tem{a_1_x>3+{I zxmNL*wuJ?xx$IMO+V<8}d{MWZrH!H2ViWs>|Hfoh1Z$n;OdM~hiuf+baO%pe4$H%` z-){{shodXi{=3gBiK{w=nz6HJ4?JPLVr(rg4#qwKD<4kCz8kgiqjkiD20RpkwW%)93Ufb~d$ZdfrS+820G z{aQuXXV6BZ9fC?6JM?ycDmfP#crG_nmrlo4J|2zCY08EC{a-Y}l1~rM*B3C3Qu0VE zP|r_?rDCE#TDXm1To;PA9G-Ni6#8pRo!e4U7~rq#;N!W2l}R7>Y{vEKXQApv9D`#{ zS&1irXU!Zc(y^yc!npdmjWN}loYRY=MX{~_s;os5i}72W2cxG(^Wt`l7g(P}WZ@RC z(BaKLsL7Iz_J)mw;je)vVL z6)|gRH~*oyCosrmT&IODO1-K*uNyMNh&|wEC&H!Hq6B%;#>I&TnA5}Ne%+ldr;t`G z&Lx>byMt2KCq1!M!4;*wBWVM4-vWTBqPMKk)nI{EcwSh9oAY@N`LKclS!-aF1?2-#tRV){r6d z!b$ zb1_8m`TFc*P6xjGYu_e%qX5#8Mm}ND(t!H#`85H`<;ER)DA7;W<-lBh**$5p6w-_E zb&pW*5~QoM#Bi4_9ey_OjFB{eoltoBr^H-8A7AD04Sd+585B8%`ZRli=UtG7c=Q0) z74ZyoMSoG!1SH{IM=hV%2Ii-YyDTxN2)P=>ckfHZ$<3QTI~G*<#6KGvCfD#hym{am zpb4}uX7%^0j)c7Gm`{EewcPg5LnivcW$F>Ea1~$b_;z}_w}xOaK+%*2bgprnhq(x-{AZ{T$JAptGj(-nxW?a&m*{(hB>ix1f}3C6ekTkmL3;>I<{uFUrL9xi43c|l(sJoDyG zxUw%8;|Z<6Uf-Q>FC|gq+?^TI>dwTBU7qCdf>tu*fY18G@v9B!Q*&NPA8Rj8X76f*PCY9QB*sy0Q>*j(Ix)>zbeM@L*ZY(Q6AJpVWk1sQ}1yx*LQoXC91jC8sbcEX6v$^7VWe*$qND@ggU(}adw z7O61L0_fJBN_g;scET}?8T?2bk!f>kfQuhsP_0%lxKWom8NnTfv9uU-D(yd{-A7_eeCr8f1N~)pZSnS>4JGJg(`W<=F0)DD3 z<>kB5{!CO|F#ceEUN%wj6C2glO~~-$Py&ajS?G6`l#26T$cgCWuGF5&av*N~6(Z1( z88C7leDavS0JCWM-0`EN1$od%5~I3FK|D1Wyr)kQz%IFMZUUvZv4ppt*tI0OFOZch z*CTEOE60kcPtWxNXPvg>bu=^Gn|s85f!_V#+xKS~qw5WzM&MVKU19+)?6hSbV?zwx z1{EnSo)bc~_cqGE9c6l+%*fJqw-tgh)*`>B%(!ssk2kD}h zbRvn^{pkx=1iBNh)dsLAyrR(sI9gLFb~~e3^KojtC7{Ex>)r!oHww}yP2#Or;qLu6 zsLd_l5#I$2@hH~(gnzsTo-S{4aHqRoVs0vw#r~QS>-Cf{lk>h?eZ29W8ejRC-n7x0 z6MxRJaq(PdCMxObo(Kb2Ch>(gY9{1y2C?FichvOtEVO|mK+Z!k6FRsHGSvTdFi?cV!y}u%&Ct8G_Px>>Ko}_QSS2d$QBhmHzOVj(;GKtZ>&&|XY znb=DAQ*;R}2YoMze90flAjp;cxAJ)?6Eb!~&(ndD4!(NRB;P2j24RB#Cn>zKI&|)f z?rLmJUF>Q`X!n8c5|3TJO1*PqB~Z|D_F)073frDzvG`9{0jUY_=QCQG8Gma$LoKZ= zBRS|!)faO~dUA`R(bW&GnFPmsMm~W>nRs=Oq~hjsmv{frppjHkgZhlJ(_foq;g1ko zd0p$N2-^;)RoyLQQdnEWm-4O*boAL#j?)THwL+_fK1%-*~41ho`Mj2 z@%So8d=su8So>^wmIidt=V)}$$gW(cQ7sGkZ0!AI_@=Sa^>9IIfnnn$ndPTJlTpjVh2p0!c_O1VSB(5J)1R zD56*pML=3abi_upf+8Y+&in2!>sr_Tw)dA+?&q!L!OUA_UICWiH(F=2E(AC*9O=xf@)^+*4J;>>l*@}vF(xevgl{Jrv@SzCBE$XR!BXl5K|pwf#1GC1gPFbNu-~O zla4V6BbC>b(BIw(*d`bm%EbSZ{7k<%j%Bl4);!E32ZQ=2_xSK>bEDr^eLE+_0A}2X ztX&c~up!qwgtw#{{<|1t>|Pw>KhLRHT5Lt`>q5^I2#OO|1?X(<;@uv5X$;i$RC5t zVO|Vr_4T`@P!M*QJ+D&)O7=Z6t>s8DC5+>miXI{Ld#45zoT0olsMU78wl5;#F6onn z7F=SdS^bI~%VY_lzC7C&ZUw|@B4DY$sX{c38I=J8RfHM87N4Ik^o9%Bllpube zuKU@fR>jNyHrPAWD8=%fv=gQUA}ls_#$rQWHud9G`v z1QB~bx~j@aM#4?0zc34U2=l7MEBJ(b4A9nEVz8How;NbJ26ifS(*NzLF&*4M z^Z2faZHr5~QWhQN6YHLcC2>3jpXV z!$4=+c*ks&5h+JUyWU6fU8MOZUR842CNp)QzL{YC5-QxEnIn0cM!pg}SI-!{LN zBY+_v^L}eZr1~=CFB?=bQgh&L`}lJu>BrQyMRNlw#2lOu0XT?tS`<<gfX zhHno#B|;@oYI6yxVk7|Uytpz$7-#7ai8+Mk0Arrqv9rJBQZ(%g6Orj9u*eo(mjF|S zz2eI%jd3nPh+k@;qr*JFP4f&iq~_z8jsKJr3q;6|2K=u{$xOl@j%@SepE)T*i({nU zcIK0sw(?hfs4c+qukOrLeUb!6uPhC4@E0M^{m$?Be780!-SD;2wWT43aY_2yFscMq zJ5v6JbX5@Hq)A*>%5A^ka2^jCY_0<)2e$$9pGEFZ+|H#~CN$6&4P0oy#;R~XJA+#j z?{Wzs3+9yjRvBTI-1*D2PK5as+V9&m$U?<vZRs-NyZg$IV3$$O_+QrgnYfZbBQtIp{<{wac6r(2^*~gwheIPh^Cw;jSJ0% zD6D?i(4WVr)$9_2KWq@-1{@opRgDUC+4JWxDZ3c|YhukW+z%EJ9G3p9{_>JP%J@x| z;**%{iCEmy(s~7$)qin_883+ghGuQ;U(2Ez0)6xz)x}9KLf=k=vANWAuX!4+N{Y>! zAFg557~maQmj@4MskLl7twJ}&f5AO*t_*{}; zl+@DA!-St1>~Ou-Q@ku8hX`|X)iay~c)RniKp3S0;rLc*Up!ERTk(F3K2FhGc;_S- zi9=t-em&j|pPJ{>?SEAK`r^U~*Mp;%TKv)km_BoQmRS&I>zHV?ghT>0Lkb(dX+FyA zj$!7e&*8zl`$mH|sgYVUi$_ntiQxM}qamx_a?v$)ZdbufG2V4VA6(hbK*@sc z1cYzTrx5%a08q7*T%z$m#1NIn$AWk$r#dmzw?E2e*M_@P6*6%dRkel-CtEMmtayIoL84n7asVD7l05m7H6blqiE;{WWn^CUP* z1TnW_+U8KjXY;MkemdgLBRA}s-E(H6gg|j_e=`DBAZK7NUxw=B39;>{gMmB=1`^h{ z^KerxF_MAWJ^zqN7_qmCOng_20M3Ku9|UqB(i&~Or1Qm?I!*$z0WP4KWzWr?6vpXq z8d?V25K$e&I_rR?IlpOhsr`L8pFTZs{9Uk{ivwB&@Gg%RDi5wY zLDc?ydia?*{o~_fKD>RGah1lN#%tWNX(y&h5r-vN^nZH?K&K6PxRQt;g7lU=Vw zHdJi|M(5pv?1=jZ2%o=Z>E%{{%63*mUARJWxBQvbyI+OSzTJ|{FDt`3zI-$T`Li(= zeJu-g{ZacRPnY^vF+4fuB&hFF#4MAdHhWAnz9mnx6SqCx8p4lB@5r*NeSS zx-=%$Ans1bg~@Obvs#M{)g>vUe=U*l&rCApB*B(J=TLRPJWXGmhl{y=^7uA}gzmY^ z)o1e@n|Q?V^8__iKwJn|Ozd>eB%fb4J(iuCg++DjUzycaN_~`kB3Wi3BES0}b86VW zG|wJ#Rv5{030A*vjW|0l!Z|>BX>0!GLbqhG)&-@ou`fof8(e-#h*#(4Yh$FX5e`FY z+X#DJ;0pal|4qv^036WeiP#eQ4X2k8hUF?^UM2BfkY0hr%D(!0Rp$|${e&8a00yC6 ze)oV^* z8EziT4H@!p4oD2UVO7bNA|Li|xbdW41h-6#p7a|PN1421zK%Z|vG zF4c48%xcRpEhBcL6_6izR!VwIbzGcSnFGg$p4DrYi%ByHT8~OBA2HR;X8cjjBK|QX z7z^sjrQ+vxvu!h3gr~>g`n2urWKH3BDlwWDZ{U1dmgOfPYhQGoiyn}r&ehGmUeZ=V z+*)v-Jm8EZQ&m@Jk(f;-`S;qnyI%9X7}T}vR&9NhX8y!T!U}$L<*5y3O7DEc4fa=~ z4H_=h7IttZv6Y9mN=#qy=5oSShh%~EEE!T0F|fM#Mgt;7b-cYxP#1{vP@CTKEP|OQ zdewJ_Nh6IsV6E2i1@X@X(MKAA0<2r1_5h=agIa9@Ox@$Nux6%3nx(!eg`_x|XfTvP zdpXXPl%+1!ljAL|Ii&(f?7GC2e0?ElDn$3uMykXG&%f=m;Fd!`VZuCej2K>XJ1Qc= zu_P9I-0e$;b~k*?^amObs8!k7yWZW2dOMU|1%~*q$Y+24&Ne z{|!A7l`TVPDZ-w0Q~{3P-1N{XS4Q@%A5b^Ur^BhMpQ?_yu}KYu+Euaa9Q@?r>_D@E zY+QN=YwSWM54&z&_u6-b^1ovq>{$ENs?<52?pk+-l1AvdeJ`h69NYd5pny%DzH$PI+h$7l*miX0~ z=np@gRk4@vjXb*BEQiLvy>MV1gparK+DP+|mI8QaMy%HW7f-d(+?gC~2){mrguleg zh-K49OJ34t?rB!IwQJMWzST4?Xah%-46_-nP%SW!x2o}5#OVCQj_lM}*pXVJ zWy|EmtDSvUfKds?$>r*ek>*UQB;D`DZB}}8_a2XBmdndA@Tmi>fL;hRO{9VDYpYPM zFY|rg6_&?C5{-U^xbplTxFx1_xYvYtukUz0tX3o^__!{7Sk0>WS4mwYvJa$JXGiat5YM89yJ(yLKSqFMZo1MM7HLYCU@=v2KDnEV#$AFVH< z9)f;xHJX=4Z@E)nuNQELu`c&7ODj3Zv7`B`MD-H1gI-$@5}Qx1^}u|5bgB^j*8i2m z{ox|Q8-3b;pSdpaQ7xLufz<{0NHaC=bWc7Wcwx(RK4nOVf9(#M2pizz4jpa` z4;ii@B$&@HNEZdjzkC@7I_o9)70Er1fTt`IyF!0m0+%3aLIyURy;4NG zg0nsmW2PX~bPaPx`dOryN=>j-SBL=*DQCyy6v#K>&18+wEwlaZ7yl-Y+$cLD-VD1Jo?aPa2~15D-B(}%wDF8mCnSjtC~n zSWOg2T@1}v{6Iv$8c2d@V3Rv2|>P^Fa1qC>Qw~zS~FpE+!=_ae-7ZPB*`tQ;7 zxwPKUVsOCYeDZ|(;Po(WUh?K~5yl6%O(tj?21IK2al(+}~m zE2Onw^;@~5(kZQ-xQIu34fM*LDnv++&VQ`*S|MU=b>rqCt%Te$qHKkZ3Q;R3YZitC z62cU9uFZ^Bim#c^QYZ#_DG&4Zo&B7}pxbFyqZTf5aHTBVQvcHvfA?1syynh zwqGTl=sa2@ptfx7p|YPHRYOii<)lICLi6=^S!vBCt?%S6tTf1R;fLt2Icf09Mw$OH zHa!c)jfX;6$%Lq*KJ)*3=PCb-O-e?pqv?*=j%7J?eO%GuEVpcWO0b)^&GK9-yb{xR zNWi3Tvd;nhnsZX?^Bs0{{>Y`*1U*e3>dQ&{1bcg$`70~=a!v5fzIEBD_I{CUgLMr0 z_G42%Yud7td45T2QzDtkwjsB^DEDWloVmf+{h~CN1{s3C+wqn~3qREJ>RCM_IcAyo zd{{{yy`-ov&Qr@w_I@&s-1#&+^?a}SZ%71_{%YZ`f@uLG9paL6UTTt=#@!tiu-z~_ zomZ9+U>cZ})?=OSAIZr|cdy>vm~bmI%~!QCy?=jJda7s03h&jI(|va@OUv-cyu57L jZqKcjmot{-@Al+cF6H@_OYwjC|BFla|2r8O|3Cg8=eS_+ literal 0 HcmV?d00001 diff --git a/Tests/images/ati2.png b/Tests/images/ati2.png new file mode 100644 index 0000000000000000000000000000000000000000..ac166965944dbfc5f0f6adf43a8c4593b6564e3b GIT binary patch literal 28408 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%0AZtXOaoytljRcS#p6{ob-0~K|ppPSLo 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 f-Gc2k+VKAe$zhIzoL_Oj00000NkvXXu0mjf6s1 Date: Sat, 23 Jul 2022 01:02:06 +0300 Subject: [PATCH 103/242] Add test and test files for ATI1 pixel format and fix image mode for ATI1 --- Tests/images/ati1.dds | Bin 0 -> 11064 bytes Tests/images/ati1.png | Bin 0 -> 1134 bytes Tests/test_file_dds.py | 13 +++++++++++++ src/PIL/DdsImagePlugin.py | 2 +- 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Tests/images/ati1.dds create mode 100644 Tests/images/ati1.png diff --git a/Tests/images/ati1.dds b/Tests/images/ati1.dds new file mode 100644 index 0000000000000000000000000000000000000000..dc9445d89ab698838d671e42281af3905b23132a GIT binary patch literal 11064 zcmZ>930A0KU|?Vu;9_V1(jd&B03sL|I3N_!qykVS3lKYocp75U#3A5-O?XsnGz3ON zU^E0qLtr!n23-g+F?dc=;S^#UbPl7jgIIz7cIlj~VDjB8$0ct^dr<4P+}j&uZ-T^C zJe$;LZ2f>6|DMrvUdzde0{_~Zv>Jpp%pBg#Hu$K$e>>OXZN3uyf9k>EP%uevz#U3u z2LSywi=}h6j{HTpSFPt3Z@-fx@beci%I}4WiG=5u3NQQZyHDj+zn$QfEq@Qyf3Rm} z@N80XoMc92XAGEe%u!}J(rxKG{uT$yU%VIf>}7hc(%wg&;eT^KZsAK>=o$O&&3rq} zP{t>J>*uRitom;9ZFB97^|Ch^LHWXClFEQNoJtl8-ZV?nH_N*HKk9(Rrfl(`AE){5 zZi-Hy`hQy8!~#RN#Q$E85ASSd>9G6s=l1zV+uO`)hB-Gjf_ne=2p2f*f=XM+Ku<WPbxJ}4>oP;=e-T&Gco`ZJ20GZ=^HNMz$dw`Y#ZFO<>V~yDj^@+apz>mv1lFoV={o`F5KuHMxXhn> zb2!C0$S+JXW;us}@qR>pVnWB1w%kuC&qA2~eF*tk)xff5!g||(n=dG>WfO2H|I{LT zb9(KK;pYE2jGl9jc+L>Hk^3R~_tx`P8?Air*L=Nr^=t1OXAg<3W!rxLIBfrOvb|+e z|KFR_{|=k@XU^Z2c(^Uq!ud^D)Bm6U|Nou5fce0I9pSH&%aZ=DUe5U=Z0iReZBwrQ z-&rIU2sJw~-E+p;a%lCA%(YUo?GPhrLS?+bo?yuFiU^5^>7 zEY(xkOCR{pe|CZY$^U;00t`T8^V@RH`O3dCtPI{wDrJ*Yhg&`v^$&eR08+0`Qn<$m ztXK1mEWq`UuMpkp*E`If^HNSu75JNeK{ssc%q6;~HvGM5KX*UaT^>R-OWz=WF7ceO zKlG)_k+1!`l*X>wWNSQoon) zFW77U=hjC0kJBl!^&}xYnwLOP`Nrnj&UO+yn?x2@? z=5(MVx8+(eU*E=iVB6d<$hbgc2hCm^;K*IWA z{$o}J*`Ig>yLn!SsRc;hdzRxEpA1NA69=6TnK;j71-)@3I;&UHdScoczL5 S$VP~Paly8%)!VqZAUXlo!UFXG literal 0 HcmV?d00001 diff --git a/Tests/images/ati1.png b/Tests/images/ati1.png new file mode 100644 index 0000000000000000000000000000000000000000..da475dfd7b01c0c4ae1a8235ce03e641948e040a GIT binary patch literal 1134 zcmV-!1d;oRP)+TM;^CeW+LlqUaDGMnwc)_T&V?Au6~R zMIWRvpF3fUp*UgKStnxFYFDXSTM26hHffS3wd7Qgnl^qDHsTwfR~R<0~+|Id&?C6&Baa#`!krJPr0N zpR8CJUICxq8K3eK2;9hWjny4H0!}A^=f^LUQd$H7-$*g{gIi@xX8*Tv>PLDUt2qvNX#|3`pizS zT|N`nGB{W$peW|;)|pWt)j(gYvl5tnCX<1MB~Z3=`Ub=4y8zh@P5TZ~5X$@R@PtX2iBRZA65I3=#?`QZoStp?3Yl!MP3%DvP z?*QLjSOSC5-5bqjnE)KtqgP$pLR}qtV~J_Cgh&WOd|sURa7rXaGAXWFeBjK1 zcSSOpJYS^X0DL8WJ6_EA@K#m_;>U)fSo@QE1Or>K+D{^pNF)-8L?V$$Boc{4B9TZW z5=jA2t-g4a<77cKQ6a;nh{q_PnhksY1T+;PP=p18;aq1czix8r(n|huA64MJ567FT zmTgy}94afD`eMOClPu6^i^alW5f;CQhn&s-p}CFMhF})p$h^KE2UX5*sP9(OnvJ!!HLIo61gyPgULwnZ$ntCUwd0m>Kln}{ZAg2;cP#%Q9 zb9l>10xab(kV_Lb1K`8g5)4uUuvJijffE(wtkUWGCKy~@1)wBYrF@;$B{&r8=IMP@0pu2V1 zfgLq<=y={&GP7OpVfvbTc=bT_y$AZYi_29zw3-fz%eR-pH=Oi;k zD$gH6b^->p4>C>j)@(1th%6!iTdJvF Date: Fri, 22 Jul 2022 22:02:55 +0000 Subject: [PATCH 104/242] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_dds.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 537302a09e6..d215e901d1c 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -76,6 +76,7 @@ def test_sanity_ati1(): assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) + def test_sanity_ati2(): """Check ATI2 images can be opened""" From 4a13857aa6ac1407bbab945d4b1d781c93c2f2d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 23 Jul 2022 15:29:01 +1000 Subject: [PATCH 105/242] Switch to GitHub Actions artifacts for Windows wheels --- RELEASING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index aa7511c8a41..b0506748470 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -96,8 +96,8 @@ Released as needed privately to individual vendors for critical security-related ## Binary Distributions ### Windows -* [ ] Contact `@cgohlke` for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174. -* [ ] Download and extract tarball from `@cgohlke` and copy into `dist/` +* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) + and copy into `dist/` ### Mac and Linux * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): From 1050d13350d7f27f26c82298f1bd4ed0199ef768 Mon Sep 17 00:00:00 2001 From: REDxEYE Date: Sat, 23 Jul 2022 12:44:03 +0300 Subject: [PATCH 106/242] Replace test files with images with compatible license --- Tests/images/ati1.dds | Bin 11064 -> 2896 bytes Tests/images/ati1.png | Bin 1134 -> 969 bytes Tests/images/ati2.dds | Bin 22000 -> 87536 bytes Tests/images/ati2.png | Bin 28408 -> 0 bytes Tests/test_file_dds.py | 6 +++--- 5 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Tests/images/ati2.png diff --git a/Tests/images/ati1.dds b/Tests/images/ati1.dds index dc9445d89ab698838d671e42281af3905b23132a..747e4b1b98ae610f2fc6f70fcc1e870c63b457af 100644 GIT binary patch literal 2896 zcmeH}T}V@57{_0?H9cp}%{c|7Y_*k8hMIce2e@FnID%k)G(JZu!BHfjSQon}_@DDT=l{I#|2gm8 zoiT%MiiB_^i4-6ed89ISS@yBNkjFiA}rh?1*)ub4u{wF(%a8?~ii1PdHm-UK1 zbJDV?)Y>*WuHG)4RjOzdK_(euL<(l_PwT&toqq8cw}sT5u5Em~tI_&z*De zUQ&G8v~WH#kk>p8bsVR91NCBP zXddKp3`{F#zMPe=zePow~t{;gx;SzI^n2SUo zi9RyizBmtak(i6*1B#DI(CW-!PaeqGTiufj;yR8ToB?5u5V;QN#Y~X1cdIWBv^ujF zr{DuMaXe_h{Qc@eebN8h{=vEGTYk`#g|fAfvVaV0^Fi20hy^4ou^iM|xbqHk)q3f#3AJGe0bGRPtT literal 11064 zcmZ>930A0KU|?Vu;9_V1(jd&B03sL|I3N_!qykVS3lKYocp75U#3A5-O?XsnGz3ON zU^E0qLtr!n23-g+F?dc=;S^#UbPl7jgIIz7cIlj~VDjB8$0ct^dr<4P+}j&uZ-T^C zJe$;LZ2f>6|DMrvUdzde0{_~Zv>Jpp%pBg#Hu$K$e>>OXZN3uyf9k>EP%uevz#U3u z2LSywi=}h6j{HTpSFPt3Z@-fx@beci%I}4WiG=5u3NQQZyHDj+zn$QfEq@Qyf3Rm} z@N80XoMc92XAGEe%u!}J(rxKG{uT$yU%VIf>}7hc(%wg&;eT^KZsAK>=o$O&&3rq} zP{t>J>*uRitom;9ZFB97^|Ch^LHWXClFEQNoJtl8-ZV?nH_N*HKk9(Rrfl(`AE){5 zZi-Hy`hQy8!~#RN#Q$E85ASSd>9G6s=l1zV+uO`)hB-Gjf_ne=2p2f*f=XM+Ku<WPbxJ}4>oP;=e-T&Gco`ZJ20GZ=^HNMz$dw`Y#ZFO<>V~yDj^@+apz>mv1lFoV={o`F5KuHMxXhn> zb2!C0$S+JXW;us}@qR>pVnWB1w%kuC&qA2~eF*tk)xff5!g||(n=dG>WfO2H|I{LT zb9(KK;pYE2jGl9jc+L>Hk^3R~_tx`P8?Air*L=Nr^=t1OXAg<3W!rxLIBfrOvb|+e z|KFR_{|=k@XU^Z2c(^Uq!ud^D)Bm6U|Nou5fce0I9pSH&%aZ=DUe5U=Z0iReZBwrQ z-&rIU2sJw~-E+p;a%lCA%(YUo?GPhrLS?+bo?yuFiU^5^>7 zEY(xkOCR{pe|CZY$^U;00t`T8^V@RH`O3dCtPI{wDrJ*Yhg&`v^$&eR08+0`Qn<$m ztXK1mEWq`UuMpkp*E`If^HNSu75JNeK{ssc%q6;~HvGM5KX*UaT^>R-OWz=WF7ceO zKlG)_k+1!`l*X>wWNSQoon) zFW77U=hjC0kJBl!^&}xYnwLOP`Nrnj&UO+yn?x2@? z=5(MVx8+(eU*E=iVB6d<$hbgc2hCm^;K*IWA z{$o}J*`Ig>yLn!SsRc;hdzRxEpA1NA69=6TnK;j71-)@3I;&UHdScoczL5 S$VP~Paly8%)!VqZAUXlo!UFXG diff --git a/Tests/images/ati1.png b/Tests/images/ati1.png index da475dfd7b01c0c4ae1a8235ce03e641948e040a..790d7d7dbb2deede17643a59f2515f7dbb2e2b17 100644 GIT binary patch literal 969 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�%%^RnpX`^({= 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 YGx*xW*^;UXz65-5kb*BqhyV2d0WH*dIsgCw literal 22000 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 diff --git a/Tests/images/ati2.png b/Tests/images/ati2.png deleted file mode 100644 index ac166965944dbfc5f0f6adf43a8c4593b6564e3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28408 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%0AZtXOaoytljRcS#p6{ob-0~K|ppPSLo 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 f-Gc2k+VKAe$zhIzoL_Oj00000NkvXXu0mjf6s1 Date: Mon, 25 Jul 2022 07:53:17 +1000 Subject: [PATCH 107/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d3c96fefd39..14293586cfe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Support saving multiple MPO frames #6444 + [radarhere] + - Do not double quote Pillow version for setuptools >= 60 #6450 [radarhere] From ce7af49eed2929f2e052842a98baae6db263fa94 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Jul 2022 18:09:06 +1000 Subject: [PATCH 108/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 14293586cfe..8616ff09c68 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Do not clear GIF tile when checking number of frames #6455 + [radarhere] + - Support saving multiple MPO frames #6444 [radarhere] From 6e97da02603fcd1fe2e16f6164f737200107d924 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Tue, 26 Jul 2022 00:30:32 +0300 Subject: [PATCH 109/242] fixing xmp tag orientation generated by exiftool --- Tests/images/xmp_orientation_exifool.png | Bin 0 -> 1935 bytes Tests/test_imageops.py | 7 +++++++ src/PIL/Image.py | 6 ++++++ src/PIL/ImageOps.py | 5 +++++ 4 files changed, 18 insertions(+) create mode 100644 Tests/images/xmp_orientation_exifool.png diff --git a/Tests/images/xmp_orientation_exifool.png b/Tests/images/xmp_orientation_exifool.png new file mode 100644 index 0000000000000000000000000000000000000000..fb30199c86acbc8953d1cf16ddfa9e3ec41ece19 GIT binary patch literal 1935 zcmY+F2{hZ;9>@QxtxWn1Gu2jWOXx_a$WnUdQ2t5V` z06@{v!PW(w`C!LG_JE_;L6P4`et*gwlVAAXEGzg8a;B2x}{-B@YV%Lg*YS zj29Bj48!tph+QrgT<^G12-t22CkTgd450?lu~<7E!=J)p1;a3>kcMak2nnV6V_j_R zzaT&hhX~|w*jN;b%jF`uhDcUu019JnZjM5qLY+Eg07e*ug)=!+o&hsV2P8mYy9`@; zm|rM^&0(;ZupK7VmvxncLm;5AFK7?GmO$a< z&F8iKbD6B(wdmL!K_T9S2q}c`15?RJXrp<;mb@oa+pNAZ5oVT{7IZ}6esn!{T`N9% ze`nFJoi55XkKts6B| z2Pdr~l<<_(Gx(5BY`|M;*F#uEFcj%G(&WlJ9@6LRsrRNDa{5Z^^#nw;4nBTC4q7F0am|R}D?90? z#r)z{Ho&Yi=oKapG8f_Z2$U0$pZVzW(er*>6`FTZ>B$)M?OuBIa7|5(Q<3o9#>bBz zU%ysS+!Jvq$TcAU)9!(IKDtq%_s*fzEyU{h++1*Ycs8l6s>-E=5~$Aq%XE|}*M1cG z=Oip$uDwv!_lI%7XQH$P4>8PsHaV#S-wQV~F>wl9Q8LT5vf$7E9!b^h?70 zW;7xap)6k7Y7-zBlH`e+(uC2!MH_O(g8adL8s}DZLqkJbn-cI;whTUzya9JlPa{%w zU*NOn&vDk)dWq4PsO#6eRJ$e!c=>B3W26XyGZt0(6I*)429nlIGs$hKuOAmVB4jd| zg@uKxA2I$N#rU#P^glW_XSr&Uxjw;*;hQ&aG6o3}8_2-dffKgc=T4}U4$SuSBtCiv zA5Z|)H8rDI-t+C2w1U?UhC%OPYG&489AQ~M0F}<61x#k#Lyp5=#1_d1e z*lhNA*$Z5^Y&qHf-f-|oYsg-dxH-4kuXAtf7B4c=Rpq;U^5Vk6VQ8$g?mdP_jfaMY zhL?v&zmX!`)~3*<1aiL3T|esDHPtw{$wl&}9Uwb9lRApYlm4uWEox|NoXz~SxoHXD z2d{2RL#tX_M}tkSdI{*N%7^71Zlt8-c4`v8A(l{}px{pTB?tsUb5A9dW&w z36=}C`i1y}1fbTMYAvZ=T&XK5b%Na8e&L%(N2063z> zsy^KO80ALcUA;3;(9V*=JHtxFjAL={KYcPcHa5oLS}H2o;o-y8=>r3<@EUrFTUk!= z7OwDc7o1=Y>9T};Zql*0c+VX3$b7rR&+i;z06?O!!q|kArtBgb6r`jS6cjL-Oe0cL zV`GnO+0f8%ZEa0;(!aW-^m3)UB@VahDk~_I%#4g&tPKC4bHo?ed>E^G#_^5mquUnD zgC3cyA3hLIL$H=UNqQO3EuVl`46|;e$OXKJ9FW*49Ls5{s;EF)SzQthHW`S8qX%x~ zUcs<_>z<#!)1X00HKqBs>e#2&9e)Cwqu6GJtf1H0HDqxAfPk;y4W5rEC*^*c&Y0w@ zh5dQu!SHi|kMM`>dR(dN`ktx548kjEhL#UsPhRhV_f%ziR_cE7vtJ^e;R`vN*zMM;q%9liD=p7U(yl0Y zNPLQ>sGN4tQB6EC*;zifuRm^S@3pIylfE-C-s0xwk<|RhHx#^ctNxOcCIL^^)jI}u P{!vFevh5?HPxOBPc@S?M literal 0 HcmV?d00001 diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 87fffa7b724..855b6bccd6a 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -351,6 +351,13 @@ def check(orientation_im): transposed_im = ImageOps.exif_transpose(im) assert 0x0112 not in transposed_im.getexif() + # Orientation from "XML:com.adobe.xmp" info key (from exiftool) + with Image.open("Tests/images/xmp_orientation_exiftool.png") as im: + assert im.getexif()[0x0112] == 8 + + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() + # Orientation from "Raw profile type exif" info key # This test image has been manually hexedited from exif_imagemagick.png # to have a different orientation diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6abb1249166..816ea94db8d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1407,6 +1407,12 @@ def getexif(self): match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags) if match: self._exif[0x0112] = int(match[1]) + else: + match = re.search( + r"([0-9])", xmp_tags + ) + if match: + self._exif[0x0112] = int(match[1]) return self._exif diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index f0d4545badf..b26b1858b93 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -606,5 +606,10 @@ def exif_transpose(image): "", transposed_image.info["XML:com.adobe.xmp"], ) + transposed_image.info["XML:com.adobe.xmp"] = re.sub( + r"([0-9])", + "", + transposed_image.info["XML:com.adobe.xmp"], + ) return transposed_image return image.copy() From db20d0f8feaf2928ef68791e7e1fdffc8658cd9e Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Tue, 26 Jul 2022 00:45:23 +0300 Subject: [PATCH 110/242] fixing typo in filetest name --- ...xifool.png => xmp_tags_orientation_exiftool.png} | Bin Tests/test_imageops.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename Tests/images/{xmp_orientation_exifool.png => xmp_tags_orientation_exiftool.png} (100%) diff --git a/Tests/images/xmp_orientation_exifool.png b/Tests/images/xmp_tags_orientation_exiftool.png similarity index 100% rename from Tests/images/xmp_orientation_exifool.png rename to Tests/images/xmp_tags_orientation_exiftool.png diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 855b6bccd6a..95b49596e79 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -352,7 +352,7 @@ def check(orientation_im): assert 0x0112 not in transposed_im.getexif() # Orientation from "XML:com.adobe.xmp" info key (from exiftool) - with Image.open("Tests/images/xmp_orientation_exiftool.png") as im: + with Image.open("Tests/images/xmp_tags_orientation_exiftool.png") as im: assert im.getexif()[0x0112] == 8 transposed_im = ImageOps.exif_transpose(im) From f42e2552068dd3d6a02e1b544ff07abf08e77036 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Jul 2022 11:58:44 +1000 Subject: [PATCH 111/242] Simplified code --- src/PIL/Image.py | 10 ++-------- src/PIL/ImageOps.py | 13 +++++-------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 816ea94db8d..4eb2dead655 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1404,15 +1404,9 @@ def getexif(self): if 0x0112 not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") if xmp_tags: - match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags) + match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: - self._exif[0x0112] = int(match[1]) - else: - match = re.search( - r"([0-9])", xmp_tags - ) - if match: - self._exif[0x0112] = int(match[1]) + self._exif[0x0112] = int(match[2]) return self._exif diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index b26b1858b93..48b41d87fda 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -601,15 +601,12 @@ def exif_transpose(image): "Raw profile type exif" ] = transposed_exif.tobytes().hex() elif "XML:com.adobe.xmp" in transposed_image.info: - transposed_image.info["XML:com.adobe.xmp"] = re.sub( + for pattern in ( r'tiff:Orientation="([0-9])"', - "", - transposed_image.info["XML:com.adobe.xmp"], - ) - transposed_image.info["XML:com.adobe.xmp"] = re.sub( r"([0-9])", - "", - transposed_image.info["XML:com.adobe.xmp"], - ) + ): + transposed_image.info["XML:com.adobe.xmp"] = re.sub( + pattern, "", transposed_image.info["XML:com.adobe.xmp"] + ) return transposed_image return image.copy() From 42763400740a06009d4cbcecf32a82501fbfc154 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Jul 2022 21:32:48 +1000 Subject: [PATCH 112/242] Sorted formats by n --- src/PIL/DdsImagePlugin.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 0f2cce1e50d..bba48016140 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -82,6 +82,7 @@ DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ + # DXT1 DXT1_FOURCC = 0x31545844 @@ -155,6 +156,14 @@ def _open(self): elif fourcc == b"DXT5": self.pixel_format = "DXT5" n = 3 + elif fourcc == b"ATI1": + self.pixel_format = "BC4" + n = 4 + self.mode = "L" + elif fourcc == b"ATI2": + self.pixel_format = "BC5" + n = 5 + self.mode = "RGB" elif fourcc == b"BC5S": self.pixel_format = "BC5S" n = 5 @@ -192,14 +201,6 @@ def _open(self): raise NotImplementedError( f"Unimplemented DXGI format {dxgi_format}" ) - elif fourcc == b"ATI1": - self.pixel_format = "BC4" - n = 4 - self.mode = "L" - elif fourcc == b"ATI2": - self.pixel_format = "BC5" - n = 5 - self.mode = "RGB" else: raise NotImplementedError(f"Unimplemented pixel format {repr(fourcc)}") From 7e1261c6a0001c2302d391da24a44e2b5d669b57 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Jul 2022 22:18:39 +1000 Subject: [PATCH 113/242] Simplified test code --- .../images/xmp_tags_orientation_exiftool.png | Bin 1935 -> 1258 bytes Tests/test_imageops.py | 16 +++++----------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Tests/images/xmp_tags_orientation_exiftool.png b/Tests/images/xmp_tags_orientation_exiftool.png index fb30199c86acbc8953d1cf16ddfa9e3ec41ece19..10f0f44009ae9237c1a5712ae8b0c7269b527711 100644 GIT binary patch literal 1258 zcmbtUO=uHA6rN($novXq`-gO`;GVjFO|jV~DKV)grKy@mnu=s}AL+KVR@ylDkNREki%i75Ie4RNh42p)E4-uJ%wX1=%ccJlDR z!FpG-3n5e=jfCSclDVsH6THt0&mY22n+?W-2+dt^f0s5wOr;|67(%7J2u++t=o>&2 zpAZ`75Ly~RNW6+rgFf@%hz}B5^vJM@kb4JtYS8pdBSIUN>hyjw(_Xi(;=WXA+v0r+9(!cxVtY(it%xK2VW> z$WM=1mM*evp-^B79gJpVS>EgQvYfyQLOU?p&7x{acDris0R#cAkz!yu5IE?+fSxe5WW0qtbW=ROn6y3mP*+Hj|{R<<9 zFN22+8AItwkpmqSq7$nUlV9m7e^a2<#IoWG7f<4zI;@g8x-E zxX)`}5nIw7MFW}`GO%Q62D!|E^2b(4NOdp*18!F{$oUBt)6#OL7?vy?5IEi=@|?(Z zQCugPbakz)b8yx)vvMtl)CbuW$7~h5p9Ta4^x>bn4=17usDH zT3r0`vte^Cb2518`jfjiw?`gvp35)47H2=-Ug&G7ZR_g`-q`iNd)u*RCw8(;^K;YQ zd#Q!D?rR@Ed}*yunypi9$0x50ox9&W*uV6q``sOWs(rcjMGfBC(%^no^pHPFw09sp IyMN@&FNzYMoB#j- delta 1598 zcmV-E2EqC236BpYiBL{Q4GJ0x0000DNk~Le0001h0000$2m$~A09~}`C;$KfiIH}Y z0y8j^2mx)AVgfFcxdIFjI6NP zABchm#igLdQV_MYhS1}}ceuxJ-!x4x$)%}}_nNA`++E-A``oWw@&`f)xKR{^Kk=`{ zvMm0@L!9ZP^!!k*e>iREVSv;jQ4VMt0J7gTwve#}S%Vup7iHfX4->MK1VB%+#xrehZ4r zj6pG=Z=(0CK%bq6(}37Tccsrx#9%;lx_4XC_}U+~q4kW~e}?`U5QXlXt4KDPUo)b% zA#wwR>+UUj&)E*=F0pI2QP}Wjcz9TNrWAqF_iX8GS&E`S2oXYtVc3#Uxm^|X<%Dn# zB3TKmsv2H>g@Lmynwb7+SynEW8yOjyn3zBa(M((`*0TslalU_vCxRnDi0%OjvaeEA zm3n{>N+y$pe~?zI)o!;f%c70fZto$ZK~yLRi-0h#-dAIH6l7nesw#vqm&;|d*|oK` zN~NOf`u6tr)YKFW0(uo|RcD>~qd`USH{?9q?T*@uURY1cv1LkQIl zXyQa7Q79Cu)#}R1imvM+)dv6^9v*^kU+uv>WS+k89RWP`^XP3H!zKU#LO3}&S+CdG zc9h9vYPA|6#QrC>VQXs(0B~?{;H%C($>;Npe@3I(Z1z701TVzAo4jMi1it-veSK9F zC7n+1?(P-}g-j-+Xo}O-QZZu81y1L5e^D{Fu_xJZ0<7Ts|s%o)VJUu=2 zRp*|h)9KsWTd{faBNt)-{H%o-Pf6A{u9rJ*6Izz_^72AQ2s%7qj4PE2{a|N>e=%;i z+oe*eR;vL3ip8Sen!B>FurNM8Ua!}?EySYU3zD00F>l7n?dYGm9ZiupWMaM}Mv=E9 z!yxQbM3`;#F!#Y7=OJciX9*#>T&`3q0RWDVkIUthZ1v2W@Fc`U`j z4?dPcVP~1a@R-7O7R1hI?e2wYe~mFtr_-%gE0s#oeLyyiYpdsP_yV_1<GKjeejT)4{Q*OfugXxfz@V1O$F zE2v-Ov6PsR2E@#eeM}+ln*2KczOsz{L)y?ETwdD8ej#m$jvlYAWD`jn#Ny+{t?UKT whVTS@b+O}O>4aazyu90yowR`mUz@W30osfJ$oL$>)c^nh07*qoM6N<$f|I@g`~Uy| diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 95b49596e79..bd5f44e5008 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -345,18 +345,12 @@ def check(orientation_im): check(orientation_im) # Orientation from "XML:com.adobe.xmp" info key - with Image.open("Tests/images/xmp_tags_orientation.png") as im: - assert im.getexif()[0x0112] == 3 - - transposed_im = ImageOps.exif_transpose(im) - assert 0x0112 not in transposed_im.getexif() + for suffix in ("", "_exiftool"): + with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im: + assert im.getexif()[0x0112] == 3 - # Orientation from "XML:com.adobe.xmp" info key (from exiftool) - with Image.open("Tests/images/xmp_tags_orientation_exiftool.png") as im: - assert im.getexif()[0x0112] == 8 - - transposed_im = ImageOps.exif_transpose(im) - assert 0x0112 not in transposed_im.getexif() + transposed_im = ImageOps.exif_transpose(im) + assert 0x0112 not in transposed_im.getexif() # Orientation from "Raw profile type exif" info key # This test image has been manually hexedited from exif_imagemagick.png From bac83f7dd3ac738f8bc16a6647f69f373916ae2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Jul 2022 22:27:43 +1000 Subject: [PATCH 114/242] Check that orientation is still absent after reloading Exif --- Tests/test_imageops.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index bd5f44e5008..01e40e6d4d5 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -352,6 +352,9 @@ def check(orientation_im): transposed_im = ImageOps.exif_transpose(im) assert 0x0112 not in transposed_im.getexif() + transposed_im._reload_exif() + assert 0x0112 not in transposed_im.getexif() + # Orientation from "Raw profile type exif" info key # This test image has been manually hexedited from exif_imagemagick.png # to have a different orientation From 78a6bb4c992664904896c37f20100225023a7300 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Jul 2022 22:33:27 +1000 Subject: [PATCH 115/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8616ff09c68..ed157abec29 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457 + [REDxEYE, radarhere] + - Do not clear GIF tile when checking number of frames #6455 [radarhere] From cbe292212b96a27aa82501c89596efb13c263d59 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Jul 2022 08:35:10 +1000 Subject: [PATCH 116/242] Added release notes for #6457 --- docs/releasenotes/9.3.0.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index da045a50a98..c64423b0152 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -53,7 +53,7 @@ TODO Other Changes ============= -TODO -^^^^ +Added DDS ATI1 and ATI2 reading +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Support has been added to read the ATI1 and ATI2 formats of DDS images. From f2ce07cf228024d7d35d31c74fa385833262c9d6 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 30 Jul 2022 13:29:10 +1000 Subject: [PATCH 117/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ed157abec29..b821e7732f6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Parse orientation from XMP tag contents #6463 + [bigcat88, radarhere] + - Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457 [REDxEYE, radarhere] From 5cc9ab5b1d78dd154d7a60883acb3d7fa5fd09c3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Aug 2022 08:55:31 +1000 Subject: [PATCH 118/242] Updated harfbuzz to 5.1.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index bc5fb4d024b..b1e6e4b8e7a 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -281,9 +281,9 @@ def cmd_msbuild( "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/4.4.1.zip", - "filename": "harfbuzz-4.4.1.zip", - "dir": "harfbuzz-4.4.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/5.1.0.zip", + "filename": "harfbuzz-5.1.0.zip", + "dir": "harfbuzz-5.1.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), From f5b27f90f7efcf01a68f7e3d84531d03e9ebfc5e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Aug 2022 20:38:47 +1000 Subject: [PATCH 119/242] Save 1 mode PDF using CCITTFaxDecode filter --- Tests/test_file_pdf.py | 2 +- src/PIL/PdfImagePlugin.py | 44 +++++++++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index c71d4f5f22b..310619fb255 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -43,7 +43,7 @@ def test_monochrome(tmp_path): # Act / Assert outfile = helper_save_as_pdf(tmp_path, mode) - assert os.path.getsize(outfile) < 15000 + assert os.path.getsize(outfile) < 5000 def test_greyscale(tmp_path): diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 2109a6f52cb..d1b34be487e 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -21,10 +21,11 @@ ## import io +import math import os import time -from . import Image, ImageFile, ImageSequence, PdfParser, __version__ +from . import Image, ImageFile, ImageSequence, PdfParser, TiffImagePlugin, __version__ # # -------------------------------------------------------------------- @@ -123,8 +124,26 @@ def _save(im, fp, filename, save_all=False): params = None decode = None + # + # Get image characteristics + + width, height = im.size + if im.mode == "1": - filter = "DCTDecode" + filter = "CCITTFaxDecode" + bits = 1 + params = PdfParser.PdfArray( + [ + PdfParser.PdfDict( + { + "K": -1, + "BlackIs1": True, + "Columns": width, + "Rows": height, + } + ) + ] + ) colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "L": @@ -161,6 +180,11 @@ def _save(im, fp, filename, save_all=False): if filter == "ASCIIHexDecode": ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + elif filter == "CCITTFaxDecode": + original_strip_size = TiffImagePlugin.STRIP_SIZE + TiffImagePlugin.STRIP_SIZE = math.ceil(im.width / 8) * im.height + im.save(op, "TIFF", compression="group4") + TiffImagePlugin.STRIP_SIZE = original_strip_size elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) elif filter == "FlateDecode": @@ -170,22 +194,24 @@ def _save(im, fp, filename, save_all=False): else: raise ValueError(f"unsupported PDF filter ({filter})") - # - # Get image characteristics - - width, height = im.size + stream = op.getvalue() + if filter == "CCITTFaxDecode": + stream = stream[8:] + filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + else: + filter = PdfParser.PdfName(filter) existing_pdf.write_obj( image_refs[page_number], - stream=op.getvalue(), + stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), Width=width, # * 72.0 / resolution, Height=height, # * 72.0 / resolution, - Filter=PdfParser.PdfName(filter), + Filter=filter, BitsPerComponent=bits, Decode=decode, - DecodeParams=params, + DecodeParms=params, ColorSpace=colorspace, ) From 2b14d83549b2100c5d08dd8cd9231dd53dde377b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Aug 2022 21:41:17 +1000 Subject: [PATCH 120/242] Added strip_size as TIFF encoder argument --- Tests/test_file_libtiff.py | 12 ++++++++---- src/PIL/PdfImagePlugin.py | 13 ++++++++----- src/PIL/TiffImagePlugin.py | 3 ++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index a43548ae0f3..3084425a406 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1011,14 +1011,18 @@ def test_save_multistrip(self, compression, tmp_path): # Assert that there are multiple strips assert len(im.tag_v2[STRIPOFFSETS]) > 1 - def test_save_single_strip(self, tmp_path): + @pytest.mark.parametrize("argument", (True, False)) + def test_save_single_strip(self, argument, tmp_path): im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") - TiffImagePlugin.STRIP_SIZE = 2**18 + if not argument: + TiffImagePlugin.STRIP_SIZE = 2**18 try: - - im.save(out, compression="tiff_adobe_deflate") + arguments = {"compression": "tiff_adobe_deflate"} + if argument: + arguments["strip_size"] = 2**18 + im.save(out, **arguments) with Image.open(out) as im: assert len(im.tag_v2[STRIPOFFSETS]) == 1 diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index d1b34be487e..181a05b8d26 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,7 +25,7 @@ import os import time -from . import Image, ImageFile, ImageSequence, PdfParser, TiffImagePlugin, __version__ +from . import Image, ImageFile, ImageSequence, PdfParser, __version__ # # -------------------------------------------------------------------- @@ -181,10 +181,13 @@ def _save(im, fp, filename, save_all=False): if filter == "ASCIIHexDecode": ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) elif filter == "CCITTFaxDecode": - original_strip_size = TiffImagePlugin.STRIP_SIZE - TiffImagePlugin.STRIP_SIZE = math.ceil(im.width / 8) * im.height - im.save(op, "TIFF", compression="group4") - TiffImagePlugin.STRIP_SIZE = original_strip_size + im.save( + op, + "TIFF", + compression="group4", + # use a single strip + strip_size=math.ceil(im.width / 8) * im.height, + ) elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) elif filter == "FlateDecode": diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0dd49340d4b..da33cc5a501 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1684,7 +1684,8 @@ def _save(im, fp, filename): stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) # aim for given strip size (64 KB by default) when using libtiff writer if libtiff: - rows_per_strip = 1 if stride == 0 else min(STRIP_SIZE // stride, im.size[1]) + im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) + rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) # JPEG encoder expects multiple of 8 rows if compression == "jpeg": rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) From 82974a404c4df2f4fac63c1a6c3a4607217a847b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 18:14:07 +0000 Subject: [PATCH 121/242] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/Lucas-C/pre-commit-hooks: v1.2.0 → v1.3.0](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.2.0...v1.3.0) - [github.com/PyCQA/flake8: 4.0.1 → 5.0.2](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.2) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5e1f3557f2..1bb71bd72ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,13 +19,13 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.2.0 + rev: v1.3.0 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.2 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] From 8464ed423b20741a6f71385ba03d56cea91c455f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Aug 2022 07:46:41 +1000 Subject: [PATCH 122/242] Updated Valgrind job to Jammy --- .github/workflows/test-valgrind.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 013e5ca4ac3..dda1b357785 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: docker: [ - ubuntu-20.04-focal-amd64-valgrind, + ubuntu-22.04-jammy-amd64-valgrind, ] dockerTag: [main] From 1112ad67a35eb25d0363d68227fbbb3eea9f2b36 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Aug 2022 14:18:31 +1000 Subject: [PATCH 123/242] Document that orientation data is removed by exif_transpose() --- src/PIL/ImageOps.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 48b41d87fda..44214fead5e 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -572,8 +572,11 @@ def solarize(image, threshold=128): def exif_transpose(image): """ - If an image has an EXIF Orientation tag, return a new image that is - transposed accordingly. Otherwise, return a copy of the image. + If an image has an EXIF Orientation tag return a new image that is + transposed accordingly. The new image will have the orientation data + removed. + + Otherwise, return a copy of the image. :param image: The image to transpose. :return: An image. From 1197e1998214ca54e41772d2f804b02e528a7bab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Aug 2022 14:19:17 +1000 Subject: [PATCH 124/242] Document that exif_transpose() does not change orientations of 1 --- src/PIL/ImageOps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 44214fead5e..0c3f900caac 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -572,9 +572,9 @@ def solarize(image, threshold=128): def exif_transpose(image): """ - If an image has an EXIF Orientation tag return a new image that is - transposed accordingly. The new image will have the orientation data - removed. + If an image has an EXIF Orientation tag, other than 1, return a new image + that is transposed accordingly. The new image will have the orientation + data removed. Otherwise, return a copy of the image. From 101f1158534f77594d6383125b4a16652d43ae91 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Aug 2022 20:03:24 +1000 Subject: [PATCH 125/242] Increased tolerance to allow for libtiff with libjpeg-turbo --- Tests/test_file_libtiff.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index a43548ae0f3..01f29fbd150 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -856,7 +856,7 @@ def test_strip_cmyk_16l_jpeg(self): def test_strip_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" @@ -864,7 +864,7 @@ def test_strip_ycbcr_jpeg_2x2_sampling(self): def test_strip_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: - assert_image_equal_tofile(im, "Tests/images/flower2.jpg") + assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) def test_tiled_cmyk_jpeg(self): infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" @@ -877,7 +877,7 @@ def test_tiled_cmyk_jpeg(self): def test_tiled_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: - assert_image_equal_tofile(im, "Tests/images/flower2.jpg") + assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" @@ -885,7 +885,7 @@ def test_tiled_ycbcr_jpeg_1x1_sampling(self): def test_tiled_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: - assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) + assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) def test_strip_planar_rgb(self): # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ From e77a7b6b4f0b496a70cabd8360f0988d32bea063 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Aug 2022 23:29:58 +1000 Subject: [PATCH 126/242] Added support for RGBA PSD images --- Tests/images/rgba.psd | Bin 0 -> 2448 bytes Tests/test_file_psd.py | 7 ++++++- src/PIL/PsdImagePlugin.py | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Tests/images/rgba.psd diff --git a/Tests/images/rgba.psd b/Tests/images/rgba.psd new file mode 100644 index 0000000000000000000000000000000000000000..45fb7c3cca0cbae6a57dc605931f9abcbba65013 GIT binary patch literal 2448 zcmcC;3J7LkWPkt`Ae92f91P4*F&PUdPhaM@V4eVwWCTM5{RSxZ) z8MZ0E)<#kC-9HyiiMjyLSY%W1C^>5gw1|Uw!%uTwF$)WhOkNXUK*X#b5%f{(auzz$&qhRY}%4S~TI0_+UbffV{|?*E2SL~IB!G|U}EqaiTVLf|38 P-Tx068b%~V>kJD3HVMyL literal 0 HcmV?d00001 diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index b4b5b7a0c65..4f934375c7c 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -4,7 +4,7 @@ from PIL import Image, PsdImagePlugin -from .helper import assert_image_similar, hopper, is_pypy +from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy test_file = "Tests/images/hopper.psd" @@ -107,6 +107,11 @@ def test_open_after_exclusive_load(): im.load() +def test_rgba(): + with Image.open("Tests/images/rgba.psd") as im: + assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") + + def test_icc_profile(): with Image.open(test_file) as im: assert "icc_profile" in im.info diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 04c2e4fe379..bd10e3b95dd 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -75,6 +75,9 @@ def _open(self): if channels > psd_channels: raise OSError("not enough channels") + if mode == "RGB" and psd_channels == 4: + mode = "RGBA" + channels = 4 self.mode = mode self._size = i32(s, 18), i32(s, 14) From 61ec41511da16f8083e8ebab1c60c5b60625a336 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Aug 2022 15:40:10 +1000 Subject: [PATCH 127/242] Updated libwebp to 1.2.4 --- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/depends/install_webp.sh b/depends/install_webp.sh index ed17f2228fc..05867b7d448 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.2.3 +archive=libwebp-1.2.4 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b1e6e4b8e7a..d46c1a40911 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -157,9 +157,9 @@ def cmd_msbuild( # "bins": [r"libtiff\*.dll"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.3.tar.gz", - "filename": "libwebp-1.2.3.tar.gz", - "dir": "libwebp-1.2.3", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz", + "filename": "libwebp-1.2.4.tar.gz", + "dir": "libwebp-1.2.4", "build": [ cmd_rmdir(r"output\release-static"), # clean cmd_nmake( From 04d976131673b94c6065d5cf92b0eab53c4469f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Aug 2022 17:29:44 +1000 Subject: [PATCH 128/242] Changed "font" to class variable --- Tests/test_imagedraw.py | 17 +++++++++++++++++ src/PIL/ImageDraw.py | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 23bc756bb14..961b4d08130 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1314,6 +1314,23 @@ def test_stroke_multiline(): assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) +def test_setting_default_font(): + # Arrange + im = Image.new("RGB", (100, 250)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + ImageDraw.ImageDraw.font = font + + # Assert + try: + assert draw.getfont() == font + finally: + ImageDraw.ImageDraw.font = None + assert isinstance(draw.getfont(), ImageFont.ImageFont) + + def test_same_color_outline(): # Prepare shape x0, y0 = 5, 5 diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 8970471d3b2..712ec6e0912 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -46,6 +46,8 @@ class ImageDraw: + font = None + def __init__(self, im, mode=None): """ Create a drawing instance. @@ -86,7 +88,6 @@ def __init__(self, im, mode=None): else: self.fontmode = "L" # aliasing is okay for other modes self.fill = 0 - self.font = None def getfont(self): """ From 42a5a743c18d87d9c54ed5ff11303caf9fcd0b4b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Aug 2022 22:48:10 +1000 Subject: [PATCH 129/242] Note to Windows users that FreeType will keep the font file open --- src/PIL/ImageFont.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a3b711c6077..efd702b8685 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -906,9 +906,10 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): This function loads a font object from the given file or file-like object, and creates a font object for a font of the given size. - Pillow uses FreeType to open font files. If you are opening many fonts - simultaneously on Windows, be aware that Windows limits the number of files - that can be open in C at once to 512. If you approach that limit, an + Pillow uses FreeType to open font files. On Windows, be aware that FreeType + will keep the file open as long as the FreeTypeFont object exists. Windows + limits the number of files that can be open in C at once to 512, so if many + fonts are opened simultaneously and that limit is approached, an ``OSError`` may be thrown, reporting that FreeType "cannot open resource". This function requires the _imagingft service. From c24b6ef4f095ba2b9e3f35d8f470d931b1310a11 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Aug 2022 23:01:36 +1000 Subject: [PATCH 130/242] Document workaround --- src/PIL/ImageFont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index efd702b8685..9386d008602 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -911,6 +911,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): limits the number of files that can be open in C at once to 512, so if many fonts are opened simultaneously and that limit is approached, an ``OSError`` may be thrown, reporting that FreeType "cannot open resource". + A workaround would be to copy the file(s) into memory, and open that instead. This function requires the _imagingft service. From 5d71ba3ca140914ff05ad8246b6d0a7053216556 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Aug 2022 09:13:06 +1000 Subject: [PATCH 131/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b821e7732f6..5f99d9d254e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Save 1 mode PDF using CCITTFaxDecode filter #6470 + [radarhere] + +- Added support for RGBA PSD images #6481 + [radarhere] + - Parse orientation from XMP tag contents #6463 [bigcat88, radarhere] From 8135bd5cfbfa1e9eacd7c24adbbfac14bb92c9e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Aug 2022 10:35:44 +1000 Subject: [PATCH 132/242] Added documentation --- docs/reference/ImageDraw.rst | 7 ++++++- docs/releasenotes/9.3.0.rst | 10 ++++++++++ src/PIL/ImageDraw.py | 5 +++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index c2d72c804c1..1ef9079fba0 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -64,7 +64,7 @@ Fonts PIL can use bitmap fonts or OpenType/TrueType fonts. -Bitmap fonts are stored in PIL’s own format, where each font typically consists +Bitmap fonts are stored in PIL's own format, where each font typically consists of two files, one named .pil and the other usually named .pbm. The former contains font metrics, the latter raster data. @@ -146,6 +146,11 @@ Methods Get the current default font. + To set the default font for all future ImageDraw instances:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + :returns: An image font. .. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index c64423b0152..a8db4edd655 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -26,6 +26,16 @@ TODO API Additions ============= +Allow default ImageDraw font to be set +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than specifying a font when calling text-related ImageDraw methods, or +setting a font on each ImageDraw instance, the default font can now be set for +all future ImageDraw operations. + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + Saving multiple MPO frames ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 712ec6e0912..e84dafb12e9 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -93,6 +93,11 @@ def getfont(self): """ Get the current default font. + To set the default font for all future ImageDraw instances:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + :returns: An image font.""" if not self.font: # FIXME: should add a font repository From 34591207326fbb2fdcf603324b6a0bb98726c654 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Aug 2022 20:46:58 +1000 Subject: [PATCH 133/242] Fixed writing bytes as ASCII tag --- Tests/test_file_tiff_metadata.py | 16 ++++++++++++++++ src/PIL/TiffImagePlugin.py | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index d7a0d93775d..d38c1c523ea 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -185,6 +185,22 @@ def test_iptc(tmp_path): im.save(out) +def test_writing_bytes_to_ascii(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[271] + assert tag.type == TiffTags.ASCII + + info[271] = b"test" + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[271] == "test" + + def test_undefined_zero(tmp_path): # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index da33cc5a501..b4c42799e3f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -727,7 +727,9 @@ def load_string(self, data, legacy_api=True): @_register_writer(2) def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 - return b"" + value.encode("ascii", "replace") + b"\0" + if not isinstance(value, bytes): + value = value.encode("ascii", "replace") + return value + b"\0" @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): From 84bdb635c2d144ec416382210d9825e5cdda065d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 16:36:46 +1000 Subject: [PATCH 134/242] Updated libjpeg-turbo to 2.1.4 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d46c1a40911..a381d636dd8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -108,9 +108,9 @@ def cmd_msbuild( deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.3/libjpeg-turbo-2.1.3.tar.gz/download", - "filename": "libjpeg-turbo-2.1.3.tar.gz", - "dir": "libjpeg-turbo-2.1.3", + + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download", + "filename": "libjpeg-turbo-2.1.4.tar.gz", + "dir": "libjpeg-turbo-2.1.4", "build": [ cmd_cmake( [ From 7e1a0ca54436bddfa38386a0b8ed5dc025ddee92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 18:32:29 +1000 Subject: [PATCH 135/242] Open 1 bit EPS in mode 1 --- Tests/images/1.eps | Bin 0 -> 45834 bytes Tests/test_file_eps.py | 5 +++++ src/PIL/EpsImagePlugin.py | 13 ++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 Tests/images/1.eps diff --git a/Tests/images/1.eps b/Tests/images/1.eps new file mode 100644 index 0000000000000000000000000000000000000000..727dc9b7f044a76c5021361e57abe2764cd8567b GIT binary patch literal 45834 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 N&d=2kJ?@+IzW}D#8wUUY literal 0 HcmV?d00001 diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 1790f4f7701..766c5064920 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -146,6 +146,11 @@ def test_bytesio_object(): assert_image_similar(img, image1_scale1_compare, 5) +def test_1_mode(): + with Image.open("Tests/images/1.eps") as im: + assert im.mode == "1" + + def test_image_mode_not_supported(tmp_path): im = hopper("RGBA") tmpfile = str(tmp_path / "temp.eps") diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 3b782d6b353..0e434c5c0ea 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -288,11 +288,14 @@ def _open(self): # Encoded bitmapped image. x, y, bi, mo = s[11:].split(None, 7)[:4] - if int(bi) != 8: - break - try: - self.mode = self.mode_map[int(mo)] - except ValueError: + if int(bi) == 1: + self.mode = "1" + elif int(bi) == 8: + try: + self.mode = self.mode_map[int(mo)] + except ValueError: + break + else: break self._size = int(x), int(y) From 99e401123bab56bf9c64314b506750a4ea6a9e79 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 19:46:07 +1000 Subject: [PATCH 136/242] Corrected palette size when saving --- Tests/test_file_tga.py | 12 ++++++++++++ src/PIL/TgaImagePlugin.py | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 0c8c9f30485..fff127421d2 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -123,6 +123,18 @@ def test_save(tmp_path): assert test_im.size == (100, 100) +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 0, 0] + im.putpalette(colors) + + out = str(tmp_path / "temp.tga") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + + def test_save_wrong_mode(tmp_path): im = hopper("PA") out = str(tmp_path / "temp.tga") diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 59b89e9885f..7f5075317c8 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -193,7 +193,8 @@ def _save(im, fp, filename): warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: - colormapfirst, colormaplength, colormapentry = 0, 256, 24 + palette = im.im.getpalette("RGB", "BGR") + colormapfirst, colormaplength, colormapentry = 0, len(palette) // 3, 24 else: colormapfirst, colormaplength, colormapentry = 0, 0, 0 @@ -225,7 +226,7 @@ def _save(im, fp, filename): fp.write(id_section) if colormaptype: - fp.write(im.im.getpalette("RGB", "BGR")) + fp.write(palette) if rle: ImageFile._save( From 5d4fbdfab4fa5dc05f4b3de3304fffdeddb9ff4f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 19:46:46 +1000 Subject: [PATCH 137/242] Simplified code --- src/PIL/TgaImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 7f5075317c8..cd454b755c0 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -194,9 +194,9 @@ def _save(im, fp, filename): if colormaptype: palette = im.im.getpalette("RGB", "BGR") - colormapfirst, colormaplength, colormapentry = 0, len(palette) // 3, 24 + colormaplength, colormapentry = len(palette) // 3, 24 else: - colormapfirst, colormaplength, colormapentry = 0, 0, 0 + colormaplength, colormapentry = 0, 0 if im.mode in ("LA", "RGBA"): flags = 8 @@ -211,7 +211,7 @@ def _save(im, fp, filename): o8(id_len) + o8(colormaptype) + o8(imagetype) - + o16(colormapfirst) + + o16(0) # colormapfirst + o16(colormaplength) + o8(colormapentry) + o16(0) From 55d94558fbaa809c0cc03c072bf7119fb2b27e78 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 23:14:32 +1000 Subject: [PATCH 138/242] Do not install test-image-results on GitHub Actions --- .ci/install.sh | 1 - .github/workflows/macos-install.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 16a056dd585..7ead209bec2 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -35,7 +35,6 @@ python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install test-image-results if [[ $(uname) != CYGWIN* ]]; then # TODO Remove condition when NumPy supports 3.11 diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 06b82964559..bb0bcd6803e 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -12,7 +12,6 @@ python3 -m pip install -U pytest python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install test-image-results echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg # TODO Remove condition when NumPy supports 3.11 From a37593f004247ebf69d5582524da6dc5143cb023 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 Aug 2022 14:34:42 +1000 Subject: [PATCH 139/242] Allow RGB and RGBA values for PA image putpixel --- Tests/test_image_access.py | 22 ++++++++++++++-------- docs/reference/PixelAccess.rst | 2 +- src/PIL/Image.py | 11 ++++++++--- src/PIL/PyAccess.py | 11 ++++++++--- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 617274a576d..58e78475340 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -212,11 +212,14 @@ def test_signedness(self): self.check(mode, 2**15 + 1) self.check(mode, 2**16 - 1) - def test_p_putpixel_rgb_rgba(self): - for color in [(255, 0, 0), (255, 0, 0, 255)]: - im = Image.new("P", (1, 1), 0) + @pytest.mark.parametrize("mode", ("P", "PA")) + def test_p_putpixel_rgb_rgba(self, mode): + for color in [(255, 0, 0), (255, 0, 0, 127)]: + im = Image.new(mode, (1, 1)) im.putpixel((0, 0), color) - assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + + alpha = color[3] if len(color) == 4 and mode == "PA" else 255 + assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) @pytest.mark.skipif(cffi is None, reason="No CFFI") @@ -337,12 +340,15 @@ def test_reference_counting(self): # pixels can contain garbage if image is released assert px[i, 0] == 0 - def test_p_putpixel_rgb_rgba(self): - for color in [(255, 0, 0), (255, 0, 0, 255)]: - im = Image.new("P", (1, 1), 0) + @pytest.mark.parametrize("mode", ("P", "PA")) + def test_p_putpixel_rgb_rgba(self, mode): + for color in [(255, 0, 0), (255, 0, 0, 127)]: + im = Image.new(mode, (1, 1)) access = PyAccess.new(im, False) access.putpixel((0, 0), color) - assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + + alpha = color[3] if len(color) == 4 and mode == "PA" else 255 + assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) class TestImagePutPixelError(AccessTest): diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index d2e80fb8cb7..b234b7b4efb 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -73,7 +73,7 @@ Access using negative indexes is also possible. Modifies the pixel at x,y. The color is given as a single numerical value for single band images, and a tuple for multi-band images. In addition to this, RGB and RGBA tuples - are accepted for P images. + are accepted for P and PA images. :param xy: The pixel coordinate, given as (x, y). :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4eb2dead655..f3f158db8bf 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1839,7 +1839,7 @@ def putpixel(self, xy, value): Modifies the pixel at the given position. The color is given as a single numerical value for single-band images, and a tuple for multi-band images. In addition to this, RGB and RGBA tuples are - accepted for P images. + accepted for P and PA images. Note that this method is relatively slow. For more extensive changes, use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` @@ -1864,12 +1864,17 @@ def putpixel(self, xy, value): return self.pyaccess.putpixel(xy, value) if ( - self.mode == "P" + self.mode in ("P", "PA") and isinstance(value, (list, tuple)) and len(value) in [3, 4] ): - # RGB or RGBA value for a P image + # RGB or RGBA value for a P or PA image + if self.mode == "PA": + alpha = value[3] if len(value) == 4 else 255 + value = value[:3] value = self.palette.getcolor(value, self) + if self.mode == "PA": + value = (value, alpha) return self.im.putpixel(xy, value) def remap_palette(self, dest_map, source_palette=None): diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 2a48c53f723..9a2ec48fc60 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -58,7 +58,7 @@ def __init__(self, img, readonly=False): # Keep pointer to im object to prevent dereferencing. self._im = img.im - if self._im.mode == "P": + if self._im.mode in ("P", "PA"): self._palette = img.palette # Debugging is polluting test traces, only useful here @@ -89,12 +89,17 @@ def __setitem__(self, xy, color): (x, y) = self.check_xy((x, y)) if ( - self._im.mode == "P" + self._im.mode in ("P", "PA") and isinstance(color, (list, tuple)) and len(color) in [3, 4] ): - # RGB or RGBA value for a P image + # RGB or RGBA value for a P or PA image + if self._im.mode == "PA": + alpha = color[3] if len(color) == 4 else 255 + color = color[:3] color = self._palette.getcolor(color, self._img) + if self._im.mode == "PA": + color = (color, alpha) return self.set_pixel(x, y, color) From 520fa19dab4b60d732d273aab8bff195ce5875cf Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 15 Aug 2022 09:15:35 +1000 Subject: [PATCH 140/242] Fixed formatting Co-authored-by: Hugo van Kemenade --- docs/releasenotes/9.3.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index a8db4edd655..7109a09f2b4 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -31,7 +31,7 @@ Allow default ImageDraw font to be set Rather than specifying a font when calling text-related ImageDraw methods, or setting a font on each ImageDraw instance, the default font can now be set for -all future ImageDraw operations. +all future ImageDraw operations:: from PIL import ImageDraw, ImageFont ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") From b84816c02f84bb42f440387366e391fa2ed79020 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Aug 2022 22:45:55 +1000 Subject: [PATCH 141/242] Added pa2p --- src/libImaging/Convert.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 5dc17db60d0..f0d42f7ff47 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1026,6 +1026,14 @@ pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { } } +static void +pa2p(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + } +} + static void p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; @@ -1209,6 +1217,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { convert = alpha ? pa2l : p2l; } else if (strcmp(mode, "LA") == 0) { convert = alpha ? pa2la : p2la; + } else if (strcmp(mode, "P") == 0) { + convert = pa2p; } else if (strcmp(mode, "PA") == 0) { convert = p2pa; } else if (strcmp(mode, "I") == 0) { @@ -1233,6 +1243,10 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { if (!imOut) { return NULL; } + if (strcmp(mode, "P") == 0) { + ImagingPaletteDelete(imOut->palette); + imOut->palette = ImagingPaletteDuplicate(imIn->palette); + } ImagingSectionEnter(&cookie); for (y = 0; y < imIn->ysize; y++) { From 8a60db322fb1ea752717bba94d248f9f08c38815 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Aug 2022 17:35:38 +1000 Subject: [PATCH 142/242] Copy palette when converting from P to PA --- Tests/test_image_convert.py | 6 ++++++ src/libImaging/Convert.c | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index e5639e10533..59e205084f4 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -236,6 +236,12 @@ def test_p2pa_alpha(): assert im_a.getpixel((x, y)) == alpha +def test_p2pa_palette(): + with Image.open("Tests/images/tiny.png") as im: + im_pa = im.convert("PA") + assert im_pa.getpalette() == im.getpalette() + + def test_matrix_illegal_conversion(): # Arrange im = hopper("CMYK") diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index f0d42f7ff47..bdc680be44b 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1243,7 +1243,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { if (!imOut) { return NULL; } - if (strcmp(mode, "P") == 0) { + if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) { ImagingPaletteDelete(imOut->palette); imOut->palette = ImagingPaletteDuplicate(imIn->palette); } From 6b35dc2a8ab238145460af37096c9b53a301a235 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Aug 2022 19:17:41 +1000 Subject: [PATCH 143/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5f99d9d254e..fb634eabad1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Allow default ImageDraw font to be set #6484 + [radarhere, hugovk] + - Save 1 mode PDF using CCITTFaxDecode filter #6470 [radarhere] From c463ef4fe370667f1db595a03a28516467f4c07d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Aug 2022 21:13:09 +1000 Subject: [PATCH 144/242] Fallback to not using mmap if buffer is not large enough --- Tests/images/mmap_error.bmp | Bin 0 -> 9253 bytes Tests/test_file_bmp.py | 7 +++++++ src/PIL/ImageFile.py | 3 +++ 3 files changed, 10 insertions(+) create mode 100644 Tests/images/mmap_error.bmp diff --git a/Tests/images/mmap_error.bmp b/Tests/images/mmap_error.bmp new file mode 100644 index 0000000000000000000000000000000000000000..04df163d7fed0433ac4dadaf0d0e5a42ca1c28bb GIT binary patch literal 9253 zcmbuDUrbb29>*_#1ly_)t|lZlX5%)S6}4}?uj zWNA{v!;Xo}pm5nc<9gexJ|pd+tE`r!Rj0iKWJ29{<4l3+s=pB5OO3jNe+;Z$8rN-)bZMVDrLZ zxh#+6TUHfMRqR)>U&VeE`&I0pWd9`lC)q#A{z>*vvfsykANzgm_p#r{ejocc*uTO4 z4fb!ae}nxS>_2AzG5e3%f6V@4_8+s)f0oa_&%V#T&%WIBhs>IBhs>IBhs>IBhs>IBhs>IBhs> zIBhs>IBhs>IBhs>IH6XA4v!9;4xA304xA304xA304xA304xA304xA304xA304xA30 zP9OVlI&eC0I&eC0I&eC0I&eC0I&eC0I&eC0I&eC0I&eC0I&eC0I&eC0(pi>tQYdA5 zEcW4a;dJ42;dJ42;dJ42;dJ42;dJ42;dJ42;dJ42``Cxmh0}%8h0}%8h0}%8h0}%8 zh0}%8h0}%8h0}%8h0}%8h0}%8g_HDIlD2eGC}pw_rw6A8rw6A8rw6A8rw6A8rw6A8 zrw6A8rw6A8rw6Ck$3C1MoF1GWoF1GWoF1GWoF1GWoF1GWoF1GWoF1GWoF1GWoF1HP zv*bid+R{m(WWwpg>BH&6>BH&6>BH&6>BH&6>BH&6>BH&6>BH&6>BH&wu@9#Yrw^wO zrw^wOrw^wOrw^wOrw^wOCrB*z;q>A3;q+zGvdXM##^J+f&N$9x=kn#NjQii;XS{m# zO8$XWrN$}7H>bW)+@YOA{J zaP^t$Gl!isly|OP{{HHH0RD0R6#x{MS#?ULln3CZ%$q;Sq<*hj%?+zRUAR3HDqT%$4N8;g}C3aSs_@5Df=>Y!UUsd`!{!(6s=u3GS+TSV@ z|6SrQ9l-zNeM)~z{}@fti|^txD*!+kfOY_e@c#k-IokgkfHL?3_&4LEN@BfMaYy5fN3gFMgo{2vbdnW$sdi+E9 z*YBy{(|~{DzQ&Kg!2g8!n~8md>~p95GSvBpb^ezJO#Tvp_^SXo{{(P~0KVxbe*(Cv z0^s~ra=rM6bpCtmP5u&q_(#b)d&T)#uNcpiFVFICCx7v$1NY~Qzv_VTR~<0^sss2} z%k{>;j{L=+4jgDy{t0Crjp*k%&$Dk}pZ z5U2_Snol)1)BgVcZ~FUh-Mn=RfTvIK2cSGq9^4t)RY&{l_crWnp#2Kq;E6o@bSL6u zFU#Z~{6F&#{+ImoQa3$d*D5Nqipud1gb#+SsDI;=0LL= z`1jxH$DV{C^9Mkke*jeeL!5uf z9{?#I1c35^2mt-f{ryu0;28j_{Q#)*4}i+Qf%7l<10cEUrL&jZvljkEy?&kX2VujX4v4+>udJ@r*W=HquWw*9(ts}-Pn;0{+?f+ajQW8hX=661U?9S5ZoVWF`>Fn^TiD-vM*jnq5??3~3f6_{>+0(o z>OXI21c34YYc6DQ1Mu`&CHh(U6Tn9P%75$h zWBAALufabAe+2({d%S%Z{|7@4e)t#uZ&dynf1SVf$6ooL#y_mD4|UXa)OF$C)6nzz zpYgwd|9huZR9sY4UPFEn@+W}yIQbL6oC<*RhyM=jg8)Y8Km_|s08A0Ul=wH%fYa*w zP>2BPI`AieMiqe0U$QqVF8NFLdWrvb{A-MVl>Cjq{ASJ{CV#!1&i^a?^-eSt)i1;! z0PUZ1zh~e3kwlFA6+obd;}Z!-+oSjo4-M1){M!0w{4ZV7FQ=YOJ$okp0O*}iv`)Y1 zqWwLM1aRU)G(9lSO!W0I`KSG}^0%z@O@mLr1c1C{S(FE$sZ#RSJJC>we(^u#UtC;N zyuBt+QxggDdpaDA<3D_F=-z`u^{Hwpf2q$Az`v=fvgs@QWhW91=@(x&eBDC;J>s9B z0g``0?8%*Hm3=}QK>o!he*h>S9*#=>+yVNNbU)1_{h&zNe`@Nb_&4eN%Jyx;)# z0HE@Zn)9Fk9A`h4pnmc8;u=OMgugJxhZzs<6{xQyH~BYx)pR=T&-s)3U0ppi0RIc= z{0n6-4WNEaan1HnO{69g;_ov6hU52!9}FA+H@kOyCNmff(gfw7F!{g4|0VvXn@)eF z--<>|{#`u){JG}>0Pnf`W5%ES84>Z94&Z-pSm$q9Z+7h7v4iqK1t9eknf_Cr`XsR7 zAJHIRcPV}4FZoOMlKaNOT=GxEtrFUA1JD9M2LLYqN&Fw`Pr_eyfbyodk+lC^<=;#D zFB*T2nZ64DG1@;a{*rtClI-=$&tLmz^2dKS070hde~5U@Lir;hxn*V+#*9@` zQe5%@{&q{KrKKa%(Sg590OtP7Niux^bm7g9^7owjj>@@8?ciRp16$<3asJ;_?5@~- zZ1=Ix&ZY0a4S)5yFMCZzVdK|}{;6w>`+to4fBXU{3fsls1;VU2&+#Prmz0!zP+~KV z9KoNFNVtrLNqya0%2(|E?6YG`z2heMUxo6KvRp z#|Zu@?VtDkBaT*7)tBsNSMIK?O!Q9nPJVkEB=Mh7oyfmTUB$`I%p|XqGOdzQ@~32%%Xz^6L;P!ve<%5G_{&G; zk8{@mVAD-eehq+$-tX|Aym*Jr>DYAszt=y<<+qAjst4L$bwK<9uxst3M_OAtJ38^_ z^tvhT|IOnZL;1VP-uMG>aq=P&#HKlan+Kj-4wHY{KP&&_>ce*r-@V;maa`uHZkgCK z<#gb`Ce!4P|HV6hCI9$LJgLvm$o$yC*ups0G7Dv|@-HndDg6-t+SVhjt(`5MgZPik zy0fX&!&K_s+jkZFD=H{|jPmEsb)$cc2Bh#;0nq=6-ihzVWZA9Wef?1DX7l|8$HqjX&3qrL=!S+4HTTdywQ= z<)6^!zqGXU!_r#D(WCfx4$^=TMv4aD|4#e?I3}|je`&u=-e<0@tW8Wzd^a)4xP2S{ z=@}Z3Wa#S`78XqM%AVKx&ys&B_kS(-|55J$PVWCf=tok?)fD-^`xW_5aQ{z||84T0 zCjS|(Po_rZQ{=z!p6pHjG(h}Y#a}vr|Llm;7ytb-jx)NCcdK!Yk-8?`Nu}1-#Gm{b zcf?;hfd6b#UoZY*TxccPPbREwwEqwQZ2+7FV6<~&@W&CMPo>rYpnApe{WSkL=f9f( zx@rD3&VP#br`A>$Y5x-dmH>DT!18o*=I=@Ao?MT=o5GS51D{?HNskFxyFlE3)Vfz-V6|78Cs`@h8hyi5YP-hKVrBLE(y9`U~T|D5Ij zoczU~4y0D)dhM@sFVq3?2cY&4_kUaKS=v81I65*)`%?n&ZhhnY>-?qtssnNWO#T2& zKH>gfy7Qd&&&(_*muY`W02bDbx3_+yt^H~Iha{`|#U zC!halZQEA5?IZjTwHDnVx&N=2`+r{U|6h^+qPhQnG57xq zbN{dE^S@>G(hcz^e@2`5x1o>!kE7@_vi;A0nbCiAUHs{Q@+W`BlK3y7kN@A7(P!}f zR@o;h&Z0i;2cQiA%D0W49lb*NQB@uQ)qVn?yaKp>{`z&wD*y>VwI6^b04Tq-{CxQZ S<(E}?03>_8_`3k(y!9VXh#-Uj literal 0 HcmV?d00001 diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index d58666b4476..604d54d889c 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -39,6 +39,13 @@ def test_invalid_file(): BmpImagePlugin.BmpImageFile(fp) +def test_fallback_if_mmap_errors(): + # This image has been truncated, + # so that the buffer is not large enough when using mmap + with Image.open("Tests/images/mmap_error.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") + + def test_save_to_bytes(): output = io.BytesIO() im = hopper() diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 9f08493c13f..f281b9e14c4 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -192,6 +192,9 @@ def load(self): with open(self.filename) as fp: self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) + if offset + self.size[1] * args[1] > self.map.size(): + # buffer is not large enough + raise OSError self.im = Image.core.map_buffer( self.map, self.size, decoder_name, offset, args ) From f9d33b40ad0d9a3cea4be3aa2fa65b3beb477e2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Aug 2022 12:09:47 +1000 Subject: [PATCH 145/242] Ubuntu dependencies also apply to Jammy [ci skip] --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index f147fa6a732..42cd7df9d31 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -367,7 +367,7 @@ In Alpine, the command is:: .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. -Prerequisites for **Ubuntu 16.04 LTS - 20.04 LTS** are installed with:: +Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ From 8a1837c80d8bfb616ef5d37be11522da701d5104 Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Sat, 20 Aug 2022 19:39:04 -0700 Subject: [PATCH 146/242] DOC: fix image-file-formats.rst --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1728c8e0579..7db7b117a77 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -968,7 +968,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum methods are: :data:`None`, ``"group3"``, ``"group4"``, ``"jpeg"``, ``"lzma"``, ``"packbits"``, ``"tiff_adobe_deflate"``, ``"tiff_ccitt"``, ``"tiff_lzw"``, ``"tiff_raw_16"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_thunderscan"``, - ``"webp"`, ``"zstd"`` + ``"webp"``, ``"zstd"`` **quality** The image quality for JPEG compression, on a scale from 0 (worst) to 100 From 3b4ea7c60275d5912c2954de00e444df4a841149 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Aug 2022 19:57:33 +1000 Subject: [PATCH 147/242] Do not use CCITTFaxDecode filter if libtiff is not available --- Tests/test_file_pdf.py | 4 ++-- src/PIL/PdfImagePlugin.py | 33 ++++++++++++++++++--------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 310619fb255..b27dbeedd2f 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -6,7 +6,7 @@ import pytest -from PIL import Image, PdfParser +from PIL import Image, PdfParser, features from .helper import hopper, mark_if_feature_version @@ -43,7 +43,7 @@ def test_monochrome(tmp_path): # Act / Assert outfile = helper_save_as_pdf(tmp_path, mode) - assert os.path.getsize(outfile) < 5000 + assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) def test_greyscale(tmp_path): diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 181a05b8d26..404759a7fcb 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,7 +25,7 @@ import os import time -from . import Image, ImageFile, ImageSequence, PdfParser, __version__ +from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features # # -------------------------------------------------------------------- @@ -130,20 +130,23 @@ def _save(im, fp, filename, save_all=False): width, height = im.size if im.mode == "1": - filter = "CCITTFaxDecode" - bits = 1 - params = PdfParser.PdfArray( - [ - PdfParser.PdfDict( - { - "K": -1, - "BlackIs1": True, - "Columns": width, - "Rows": height, - } - ) - ] - ) + if features.check("libtiff"): + filter = "CCITTFaxDecode" + bits = 1 + params = PdfParser.PdfArray( + [ + PdfParser.PdfDict( + { + "K": -1, + "BlackIs1": True, + "Columns": width, + "Rows": height, + } + ) + ] + ) + else: + filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "L": From fd47eed73a7aa178848f280f09435b55bbaefd69 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Aug 2022 09:23:42 -0500 Subject: [PATCH 148/242] parametrize Tests/test_image_paste.py --- Tests/test_image_paste.py | 486 +++++++++++++++++++++----------------- 1 file changed, 266 insertions(+), 220 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 4ea1d73ce16..bb01ff11067 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import CachedProperty, assert_image_equal @@ -101,226 +103,270 @@ def gradient_RGBa(self): ], ) - def test_image_solid(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "red") - im2 = getattr(self, "gradient_" + mode) - - im.paste(im2, (12, 23)) - - im = im.crop((12, 23, im2.width + 12, im2.height + 23)) - assert_image_equal(im, im2) - - def test_image_mask_1(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.mask_1, - [ - (255, 255, 255, 255), - (255, 255, 255, 255), - (127, 254, 127, 0), - (255, 255, 255, 255), - (255, 255, 255, 255), - (191, 190, 63, 64), - (127, 0, 127, 254), - (191, 64, 63, 190), - (255, 255, 255, 255), - ], - ) - - def test_image_mask_L(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.mask_L, - [ - (128, 191, 255, 191), - (208, 239, 239, 208), - (255, 255, 255, 255), - (112, 111, 206, 207), - (192, 191, 191, 191), - (239, 239, 207, 207), - (128, 1, 128, 254), - (207, 113, 112, 207), - (255, 191, 128, 191), - ], - ) - - def test_image_mask_LA(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.gradient_LA, - [ - (128, 191, 255, 191), - (112, 207, 206, 111), - (128, 254, 128, 1), - (208, 208, 239, 239), - (192, 191, 191, 191), - (207, 207, 112, 113), - (255, 255, 255, 255), - (239, 207, 207, 239), - (255, 191, 128, 191), - ], - ) - - def test_image_mask_RGBA(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.gradient_RGBA, - [ - (128, 191, 255, 191), - (208, 239, 239, 208), - (255, 255, 255, 255), - (112, 111, 206, 207), - (192, 191, 191, 191), - (239, 239, 207, 207), - (128, 1, 128, 254), - (207, 113, 112, 207), - (255, 191, 128, 191), - ], - ) - - def test_image_mask_RGBa(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "white") - im2 = getattr(self, "gradient_" + mode) - - self.assert_9points_paste( - im, - im2, - self.gradient_RGBa, - [ - (128, 255, 126, 255), - (0, 127, 126, 255), - (126, 253, 126, 255), - (128, 127, 254, 255), - (0, 255, 254, 255), - (126, 125, 254, 255), - (128, 1, 128, 255), - (0, 129, 128, 255), - (126, 255, 128, 255), - ], - ) - - def test_color_solid(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), "black") - - rect = (12, 23, 128 + 12, 128 + 23) - im.paste("white", rect) - - hist = im.crop(rect).histogram() - while hist: - head, hist = hist[:256], hist[256:] - assert head[255] == 128 * 128 - assert sum(head[:255]) == 0 - - def test_color_mask_1(self): - for mode in ("RGBA", "RGB", "L"): - im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) - color = (10, 20, 30, 40)[: len(mode)] - - self.assert_9points_paste( - im, - color, - self.mask_1, - [ - (50, 60, 70, 80), - (50, 60, 70, 80), - (10, 20, 30, 40), - (50, 60, 70, 80), - (50, 60, 70, 80), - (10, 20, 30, 40), - (10, 20, 30, 40), - (10, 20, 30, 40), - (50, 60, 70, 80), - ], - ) - - def test_color_mask_L(self): - for mode in ("RGBA", "RGB", "L"): - im = getattr(self, "gradient_" + mode).copy() - color = "white" - - self.assert_9points_paste( - im, - color, - self.mask_L, - [ - (127, 191, 254, 191), - (111, 207, 206, 110), - (127, 254, 127, 0), - (207, 207, 239, 239), - (191, 191, 190, 191), - (207, 206, 111, 112), - (254, 254, 254, 255), - (239, 206, 206, 238), - (254, 191, 127, 191), - ], - ) - - def test_color_mask_RGBA(self): - for mode in ("RGBA", "RGB", "L"): - im = getattr(self, "gradient_" + mode).copy() - color = "white" - - self.assert_9points_paste( - im, - color, - self.gradient_RGBA, - [ - (127, 191, 254, 191), - (111, 207, 206, 110), - (127, 254, 127, 0), - (207, 207, 239, 239), - (191, 191, 190, 191), - (207, 206, 111, 112), - (254, 254, 254, 255), - (239, 206, 206, 238), - (254, 191, 127, 191), - ], - ) - - def test_color_mask_RGBa(self): - for mode in ("RGBA", "RGB", "L"): - im = getattr(self, "gradient_" + mode).copy() - color = "white" - - self.assert_9points_paste( - im, - color, - self.gradient_RGBa, - [ - (255, 63, 126, 63), - (47, 143, 142, 46), - (126, 253, 126, 255), - (15, 15, 47, 47), - (63, 63, 62, 63), - (142, 141, 46, 47), - (255, 255, 255, 0), - (48, 15, 15, 47), - (126, 63, 255, 63), - ], - ) + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_solid(self, mode): + im = Image.new(mode, (200, 200), "red") + im2 = getattr(self, "gradient_" + mode) + + im.paste(im2, (12, 23)) + + im = im.crop((12, 23, im2.width + 12, im2.height + 23)) + assert_image_equal(im, im2) + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_1(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.mask_1, + [ + (255, 255, 255, 255), + (255, 255, 255, 255), + (127, 254, 127, 0), + (255, 255, 255, 255), + (255, 255, 255, 255), + (191, 190, 63, 64), + (127, 0, 127, 254), + (191, 64, 63, 190), + (255, 255, 255, 255), + ], + ) + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_L(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.mask_L, + [ + (128, 191, 255, 191), + (208, 239, 239, 208), + (255, 255, 255, 255), + (112, 111, 206, 207), + (192, 191, 191, 191), + (239, 239, 207, 207), + (128, 1, 128, 254), + (207, 113, 112, 207), + (255, 191, 128, 191), + ], + ) + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_LA(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_LA, + [ + (128, 191, 255, 191), + (112, 207, 206, 111), + (128, 254, 128, 1), + (208, 208, 239, 239), + (192, 191, 191, 191), + (207, 207, 112, 113), + (255, 255, 255, 255), + (239, 207, 207, 239), + (255, 191, 128, 191), + ], + ) + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_RGBA(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_RGBA, + [ + (128, 191, 255, 191), + (208, 239, 239, 208), + (255, 255, 255, 255), + (112, 111, 206, 207), + (192, 191, 191, 191), + (239, 239, 207, 207), + (128, 1, 128, 254), + (207, 113, 112, 207), + (255, 191, 128, 191), + ], + ) + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_image_mask_RGBa(self, mode): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_RGBa, + [ + (128, 255, 126, 255), + (0, 127, 126, 255), + (126, 253, 126, 255), + (128, 127, 254, 255), + (0, 255, 254, 255), + (126, 125, 254, 255), + (128, 1, 128, 255), + (0, 129, 128, 255), + (126, 255, 128, 255), + ], + ) + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_solid(self, mode): + im = Image.new(mode, (200, 200), "black") + + rect = (12, 23, 128 + 12, 128 + 23) + im.paste("white", rect) + + hist = im.crop(rect).histogram() + while hist: + head, hist = hist[:256], hist[256:] + assert head[255] == 128 * 128 + assert sum(head[:255]) == 0 + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_mask_1(self, mode): + im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) + color = (10, 20, 30, 40)[: len(mode)] + + self.assert_9points_paste( + im, + color, + self.mask_1, + [ + (50, 60, 70, 80), + (50, 60, 70, 80), + (10, 20, 30, 40), + (50, 60, 70, 80), + (50, 60, 70, 80), + (10, 20, 30, 40), + (10, 20, 30, 40), + (10, 20, 30, 40), + (50, 60, 70, 80), + ], + ) + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_mask_L(self, mode): + im = getattr(self, "gradient_" + mode).copy() + color = "white" + + self.assert_9points_paste( + im, + color, + self.mask_L, + [ + (127, 191, 254, 191), + (111, 207, 206, 110), + (127, 254, 127, 0), + (207, 207, 239, 239), + (191, 191, 190, 191), + (207, 206, 111, 112), + (254, 254, 254, 255), + (239, 206, 206, 238), + (254, 191, 127, 191), + ], + ) + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_mask_RGBA(self, mode): + im = getattr(self, "gradient_" + mode).copy() + color = "white" + + self.assert_9points_paste( + im, + color, + self.gradient_RGBA, + [ + (127, 191, 254, 191), + (111, 207, 206, 110), + (127, 254, 127, 0), + (207, 207, 239, 239), + (191, 191, 190, 191), + (207, 206, 111, 112), + (254, 254, 254, 255), + (239, 206, 206, 238), + (254, 191, 127, 191), + ], + ) + + @pytest.mark.parametrize("mode", [ + "RGBA", + "RGB", + "L", + ]) + def test_color_mask_RGBa(self, mode): + im = getattr(self, "gradient_" + mode).copy() + color = "white" + + self.assert_9points_paste( + im, + color, + self.gradient_RGBa, + [ + (255, 63, 126, 63), + (47, 143, 142, 46), + (126, 253, 126, 255), + (15, 15, 47, 47), + (63, 63, 62, 63), + (142, 141, 46, 47), + (255, 255, 255, 0), + (48, 15, 15, 47), + (126, 63, 255, 63), + ], + ) def test_different_sizes(self): im = Image.new("RGB", (100, 100)) From 1421f94b6de11800a5b6ecc4ef43e6eaeb039dc8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Aug 2022 14:25:29 +0000 Subject: [PATCH 149/242] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_image_paste.py | 143 +++++++++++++++++++++++--------------- 1 file changed, 88 insertions(+), 55 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index bb01ff11067..0b40ba671fb 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -103,11 +103,14 @@ def gradient_RGBa(self): ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_solid(self, mode): im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -117,11 +120,14 @@ def test_image_solid(self, mode): im = im.crop((12, 23, im2.width + 12, im2.height + 23)) assert_image_equal(im, im2) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_1(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -143,11 +149,14 @@ def test_image_mask_1(self, mode): ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_L(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -169,11 +178,14 @@ def test_image_mask_L(self, mode): ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_LA(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -195,11 +207,14 @@ def test_image_mask_LA(self, mode): ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_RGBA(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -221,11 +236,14 @@ def test_image_mask_RGBA(self, mode): ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_image_mask_RGBa(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -247,11 +265,14 @@ def test_image_mask_RGBa(self, mode): ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_solid(self, mode): im = Image.new(mode, (200, 200), "black") @@ -264,11 +285,14 @@ def test_color_solid(self, mode): assert head[255] == 128 * 128 assert sum(head[:255]) == 0 - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_mask_1(self, mode): im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -290,11 +314,14 @@ def test_color_mask_1(self, mode): ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_mask_L(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -316,11 +343,14 @@ def test_color_mask_L(self, mode): ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_mask_RGBA(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -342,11 +372,14 @@ def test_color_mask_RGBA(self, mode): ], ) - @pytest.mark.parametrize("mode", [ - "RGBA", - "RGB", - "L", - ]) + @pytest.mark.parametrize( + "mode", + [ + "RGBA", + "RGB", + "L", + ], + ) def test_color_mask_RGBa(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" From b236c61c04c0f6a6cc1ac24f5a56e327e890ad9c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 22 Aug 2022 13:29:26 -0500 Subject: [PATCH 150/242] make @pytest.mark.parametrize annotations one line --- Tests/test_image_paste.py | 99 +++++---------------------------------- 1 file changed, 11 insertions(+), 88 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 0b40ba671fb..1ab02017de1 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -103,14 +103,7 @@ def gradient_RGBa(self): ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_solid(self, mode): im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -120,14 +113,7 @@ def test_image_solid(self, mode): im = im.crop((12, 23, im2.width + 12, im2.height + 23)) assert_image_equal(im, im2) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_1(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -149,14 +135,7 @@ def test_image_mask_1(self, mode): ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_L(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -178,14 +157,7 @@ def test_image_mask_L(self, mode): ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_LA(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -207,14 +179,7 @@ def test_image_mask_LA(self, mode): ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_RGBA(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -236,14 +201,7 @@ def test_image_mask_RGBA(self, mode): ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_image_mask_RGBa(self, mode): im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -265,14 +223,7 @@ def test_image_mask_RGBa(self, mode): ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_solid(self, mode): im = Image.new(mode, (200, 200), "black") @@ -285,14 +236,7 @@ def test_color_solid(self, mode): assert head[255] == 128 * 128 assert sum(head[:255]) == 0 - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_mask_1(self, mode): im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -314,14 +258,7 @@ def test_color_mask_1(self, mode): ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_mask_L(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -343,14 +280,7 @@ def test_color_mask_L(self, mode): ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_mask_RGBA(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -372,14 +302,7 @@ def test_color_mask_RGBA(self, mode): ], ) - @pytest.mark.parametrize( - "mode", - [ - "RGBA", - "RGB", - "L", - ], - ) + @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) def test_color_mask_RGBa(self, mode): im = getattr(self, "gradient_" + mode).copy() color = "white" From be9224f28525211d88e9e769a32bed80a6480cd0 Mon Sep 17 00:00:00 2001 From: Bibin Hashley Date: Tue, 23 Aug 2022 02:57:03 +0530 Subject: [PATCH 151/242] ImageOps.contain function finding new size issue --- src/PIL/ImageOps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 0c3f900caac..61de3b696f9 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -255,11 +255,11 @@ def contain(image, size, method=Image.Resampling.BICUBIC): if im_ratio != dest_ratio: if im_ratio > dest_ratio: - new_height = int(image.height / image.width * size[0]) + new_height = round(image.height / image.width * size[0]) if new_height != size[1]: size = (size[0], new_height) else: - new_width = int(image.width / image.height * size[1]) + new_width = round(image.width / image.height * size[1]) if new_width != size[0]: size = (new_width, size[1]) return image.resize(size, resample=method) From b6b42b8e569ad42686f5522c7e4228fbf68101fd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Aug 2022 07:41:12 +1000 Subject: [PATCH 152/242] Updated libimagequant to 4.0.2 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 9b3088b9450..76f4cb95f3f 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.1 +archive=libimagequant-4.0.2 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index 42cd7df9d31..a8cd5e4415a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -166,7 +166,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.1** + * Pillow has been tested with libimagequant **2.6-4.0.2** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From a3e61c1f89ea726d011683486ce81d6c448a2374 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Aug 2022 09:16:40 +1000 Subject: [PATCH 153/242] Temporarily skip valgrind failure --- Tests/test_file_pdf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 310619fb255..df0b7abe642 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -37,6 +37,7 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): return outfile +@pytest.mark.valgrind_known_error(reason="Temporary skip") def test_monochrome(tmp_path): # Arrange mode = "1" From 0ed03d4a58d5f31d570fc9fc391298ce032ad7ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Aug 2022 21:41:32 +1000 Subject: [PATCH 154/242] Parametrize tests --- Tests/test_file_apng.py | 18 +- Tests/test_file_container.py | 132 ++++++++------- Tests/test_file_im.py | 15 +- Tests/test_file_libtiff.py | 82 ++++----- Tests/test_file_mpo.py | 195 +++++++++++---------- Tests/test_file_tga.py | 77 ++++----- Tests/test_file_wmf.py | 10 +- Tests/test_image.py | 85 +++++----- Tests/test_image_access.py | 35 ++-- Tests/test_image_convert.py | 57 +++---- Tests/test_image_copy.py | 53 +++--- Tests/test_image_crop.py | 19 +-- Tests/test_image_resample.py | 288 +++++++++++++++---------------- Tests/test_image_resize.py | 27 +-- Tests/test_image_rotate.py | 30 ++-- Tests/test_image_transpose.py | 289 +++++++++++++++----------------- Tests/test_imagedraw.py | 20 +-- Tests/test_qt_image_toqimage.py | 58 +++---- 18 files changed, 730 insertions(+), 760 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index ad61a07ccc5..d624bbb849c 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -325,8 +325,9 @@ def open(): pytest.warns(UserWarning, open) -def test_apng_sequence_errors(): - test_files = [ +@pytest.mark.parametrize( + "f", + ( "sequence_start.png", "sequence_gap.png", "sequence_repeat.png", @@ -334,12 +335,13 @@ def test_apng_sequence_errors(): "sequence_reorder.png", "sequence_reorder_chunk.png", "sequence_fdat_fctl.png", - ] - for f in test_files: - with pytest.raises(SyntaxError): - with Image.open(f"Tests/images/apng/{f}") as im: - im.seek(im.n_frames - 1) - im.load() + ), +) +def test_apng_sequence_errors(f): + with pytest.raises(SyntaxError): + with Image.open(f"Tests/images/apng/{f}") as im: + im.seek(im.n_frames - 1) + im.load() def test_apng_save(tmp_path): diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index b752e217faa..65cf6a75ea3 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,3 +1,5 @@ +import pytest + from PIL import ContainerIO, Image from .helper import hopper @@ -59,89 +61,89 @@ def test_seek_mode_2(): assert container.tell() == 100 -def test_read_n0(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_n0(bytesmode): # Arrange - for bytesmode in (True, False): - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(81) - data = container.read() + # Act + container.seek(81) + data = container.read() - # Assert - if bytesmode: - data = data.decode() - assert data == "7\nThis is line 8\n" + # Assert + if bytesmode: + data = data.decode() + assert data == "7\nThis is line 8\n" -def test_read_n(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_n(bytesmode): # Arrange - for bytesmode in (True, False): - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(81) - data = container.read(3) + # Act + container.seek(81) + data = container.read(3) - # Assert - if bytesmode: - data = data.decode() - assert data == "7\nT" + # Assert + if bytesmode: + data = data.decode() + assert data == "7\nT" -def test_read_eof(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_read_eof(bytesmode): # Arrange - for bytesmode in (True, False): - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 22, 100) + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) - # Act - container.seek(100) - data = container.read() + # Act + container.seek(100) + data = container.read() - # Assert - if bytesmode: - data = data.decode() - assert data == "" + # Assert + if bytesmode: + data = data.decode() + assert data == "" -def test_readline(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readline(bytesmode): # Arrange - for bytesmode in (True, False): - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 0, 120) + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) - # Act - data = container.readline() + # Act + data = container.readline() - # Assert - if bytesmode: - data = data.decode() - assert data == "This is line 1\n" + # Assert + if bytesmode: + data = data.decode() + assert data == "This is line 1\n" -def test_readlines(): +@pytest.mark.parametrize("bytesmode", (True, False)) +def test_readlines(bytesmode): # Arrange - for bytesmode in (True, False): - expected = [ - "This is line 1\n", - "This is line 2\n", - "This is line 3\n", - "This is line 4\n", - "This is line 5\n", - "This is line 6\n", - "This is line 7\n", - "This is line 8\n", - ] - with open(TEST_FILE, "rb" if bytesmode else "r") as fh: - container = ContainerIO.ContainerIO(fh, 0, 120) - - # Act - data = container.readlines() - - # Assert - if bytesmode: - data = [line.decode() for line in data] - assert data == expected + expected = [ + "This is line 1\n", + "This is line 2\n", + "This is line 3\n", + "This is line 4\n", + "This is line 5\n", + "This is line 6\n", + "This is line 7\n", + "This is line 8\n", + ] + with open(TEST_FILE, "rb" if bytesmode else "r") as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + # Act + data = container.readlines() + + # Assert + if bytesmode: + data = [line.decode() for line in data] + assert data == expected diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 675210c30d2..e458a197ca4 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -78,15 +78,12 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_roundtrip(tmp_path): - def roundtrip(mode): - out = str(tmp_path / "temp.im") - im = hopper(mode) - im.save(out) - assert_image_equal_tofile(im, out) - - for mode in ["RGB", "P", "PA"]: - roundtrip(mode) +@pytest.mark.parametrize("mode", ("RGB", "P", "PA")) +def test_roundtrip(mode, tmp_path): + out = str(tmp_path / "temp.im") + im = hopper(mode) + im.save(out) + assert_image_equal_tofile(im, out) def test_save_unsupported_mode(tmp_path): diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index f9d8e282647..86a0fda04e5 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -135,50 +135,50 @@ def test_adobe_deflate_tiff(self): assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_write_metadata(self, tmp_path): + @pytest.mark.parametrize("legacy_api", (False, True)) + def test_write_metadata(self, legacy_api, tmp_path): """Test metadata writing through libtiff""" - for legacy_api in [False, True]: - f = str(tmp_path / "temp.tiff") - with Image.open("Tests/images/hopper_g4.tif") as img: - img.save(f, tiffinfo=img.tag) - - if legacy_api: - original = img.tag.named() - else: - original = img.tag_v2.named() - - # PhotometricInterpretation is set from SAVE_INFO, - # not the original image. - ignored = [ - "StripByteCounts", - "RowsPerStrip", - "PageNumber", - "PhotometricInterpretation", - ] - - with Image.open(f) as loaded: - if legacy_api: - reloaded = loaded.tag.named() - else: - reloaded = loaded.tag_v2.named() - - for tag, value in itertools.chain(reloaded.items(), original.items()): - if tag not in ignored: - val = original[tag] - if tag.endswith("Resolution"): - if legacy_api: - assert val[0][0] / val[0][1] == ( - 4294967295 / 113653537 - ), f"{tag} didn't roundtrip" - else: - assert val == 37.79000115940079, f"{tag} didn't roundtrip" + f = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper_g4.tif") as img: + img.save(f, tiffinfo=img.tag) + + if legacy_api: + original = img.tag.named() + else: + original = img.tag_v2.named() + + # PhotometricInterpretation is set from SAVE_INFO, + # not the original image. + ignored = [ + "StripByteCounts", + "RowsPerStrip", + "PageNumber", + "PhotometricInterpretation", + ] + + with Image.open(f) as loaded: + if legacy_api: + reloaded = loaded.tag.named() + else: + reloaded = loaded.tag_v2.named() + + for tag, value in itertools.chain(reloaded.items(), original.items()): + if tag not in ignored: + val = original[tag] + if tag.endswith("Resolution"): + if legacy_api: + assert val[0][0] / val[0][1] == ( + 4294967295 / 113653537 + ), f"{tag} didn't roundtrip" else: - assert val == value, f"{tag} didn't roundtrip" + assert val == 37.79000115940079, f"{tag} didn't roundtrip" + else: + assert val == value, f"{tag} didn't roundtrip" - # https://github.com/python-pillow/Pillow/issues/1561 - requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] - for field in requested_fields: - assert field in reloaded, f"{field} not in metadata" + # https://github.com/python-pillow/Pillow/issues/1561 + requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] + for field in requested_fields: + assert field in reloaded, f"{field} not in metadata" @pytest.mark.valgrind_known_error(reason="Known invalid metadata") def test_additional_metadata(self, tmp_path): diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 849857d31d6..d94bdaa96c9 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -27,13 +27,13 @@ def roundtrip(im, **options): return im -def test_sanity(): - for test_file in test_files: - with Image.open(test_file) as im: - im.load() - assert im.mode == "RGB" - assert im.size == (640, 480) - assert im.format == "MPO" +@pytest.mark.parametrize("test_file", test_files) +def test_sanity(test_file): + with Image.open(test_file) as im: + im.load() + assert im.mode == "RGB" + assert im.size == (640, 480) + assert im.format == "MPO" @pytest.mark.skipif(is_pypy(), reason="Requires CPython") @@ -66,26 +66,25 @@ def test_context_manager(): im.load() -def test_app(): - for test_file in test_files: - # Test APP/COM reader (@PIL135) - with Image.open(test_file) as im: - assert im.applist[0][0] == "APP1" - assert im.applist[1][0] == "APP2" - assert ( - im.applist[1][1][:16] - == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" - ) - assert len(im.applist) == 2 +@pytest.mark.parametrize("test_file", test_files) +def test_app(test_file): + # Test APP/COM reader (@PIL135) + with Image.open(test_file) as im: + assert im.applist[0][0] == "APP1" + assert im.applist[1][0] == "APP2" + assert ( + im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" + ) + assert len(im.applist) == 2 -def test_exif(): - for test_file in test_files: - with Image.open(test_file) as im: - info = im._getexif() - assert info[272] == "Nintendo 3DS" - assert info[296] == 2 - assert info[34665] == 188 +@pytest.mark.parametrize("test_file", test_files) +def test_exif(test_file): + with Image.open(test_file) as im: + info = im._getexif() + assert info[272] == "Nintendo 3DS" + assert info[296] == 2 + assert info[34665] == 188 def test_frame_size(): @@ -137,12 +136,12 @@ def test_reload_exif_after_seek(): assert 296 in exif -def test_mp(): - for test_file in test_files: - with Image.open(test_file) as im: - mpinfo = im._getmp() - assert mpinfo[45056] == b"0100" - assert mpinfo[45057] == 2 +@pytest.mark.parametrize("test_file", test_files) +def test_mp(test_file): + with Image.open(test_file) as im: + mpinfo = im._getmp() + assert mpinfo[45056] == b"0100" + assert mpinfo[45057] == 2 def test_mp_offset(): @@ -162,48 +161,48 @@ def test_mp_no_data(): im.seek(1) -def test_mp_attribute(): - for test_file in test_files: - with Image.open(test_file) as im: - mpinfo = im._getmp() - frame_number = 0 - for mpentry in mpinfo[0xB002]: - mpattr = mpentry["Attribute"] - if frame_number: - assert not mpattr["RepresentativeImageFlag"] - else: - assert mpattr["RepresentativeImageFlag"] - assert not mpattr["DependentParentImageFlag"] - assert not mpattr["DependentChildImageFlag"] - assert mpattr["ImageDataFormat"] == "JPEG" - assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" - assert mpattr["Reserved"] == 0 - frame_number += 1 - - -def test_seek(): - for test_file in test_files: - with Image.open(test_file) as im: - assert im.tell() == 0 - # prior to first image raises an error, both blatant and borderline - with pytest.raises(EOFError): - im.seek(-1) - with pytest.raises(EOFError): - im.seek(-523) - # after the final image raises an error, - # both blatant and borderline - with pytest.raises(EOFError): - im.seek(2) - with pytest.raises(EOFError): - im.seek(523) - # bad calls shouldn't change the frame - assert im.tell() == 0 - # this one will work - im.seek(1) - assert im.tell() == 1 - # and this one, too - im.seek(0) - assert im.tell() == 0 +@pytest.mark.parametrize("test_file", test_files) +def test_mp_attribute(test_file): + with Image.open(test_file) as im: + mpinfo = im._getmp() + frame_number = 0 + for mpentry in mpinfo[0xB002]: + mpattr = mpentry["Attribute"] + if frame_number: + assert not mpattr["RepresentativeImageFlag"] + else: + assert mpattr["RepresentativeImageFlag"] + assert not mpattr["DependentParentImageFlag"] + assert not mpattr["DependentChildImageFlag"] + assert mpattr["ImageDataFormat"] == "JPEG" + assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" + assert mpattr["Reserved"] == 0 + frame_number += 1 + + +@pytest.mark.parametrize("test_file", test_files) +def test_seek(test_file): + with Image.open(test_file) as im: + assert im.tell() == 0 + # prior to first image raises an error, both blatant and borderline + with pytest.raises(EOFError): + im.seek(-1) + with pytest.raises(EOFError): + im.seek(-523) + # after the final image raises an error, + # both blatant and borderline + with pytest.raises(EOFError): + im.seek(2) + with pytest.raises(EOFError): + im.seek(523) + # bad calls shouldn't change the frame + assert im.tell() == 0 + # this one will work + im.seek(1) + assert im.tell() == 1 + # and this one, too + im.seek(0) + assert im.tell() == 0 def test_n_frames(): @@ -225,31 +224,31 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_image_grab(): - for test_file in test_files: - with Image.open(test_file) as im: - assert im.tell() == 0 - im0 = im.tobytes() - im.seek(1) - assert im.tell() == 1 - im1 = im.tobytes() - im.seek(0) - assert im.tell() == 0 - im02 = im.tobytes() - assert im0 == im02 - assert im0 != im1 - - -def test_save(): - for test_file in test_files: - with Image.open(test_file) as im: - assert im.tell() == 0 - jpg0 = roundtrip(im) - assert_image_similar(im, jpg0, 30) - im.seek(1) - assert im.tell() == 1 - jpg1 = roundtrip(im) - assert_image_similar(im, jpg1, 30) +@pytest.mark.parametrize("test_file", test_files) +def test_image_grab(test_file): + with Image.open(test_file) as im: + assert im.tell() == 0 + im0 = im.tobytes() + im.seek(1) + assert im.tell() == 1 + im1 = im.tobytes() + im.seek(0) + assert im.tell() == 0 + im02 = im.tobytes() + assert im0 == im02 + assert im0 != im1 + + +@pytest.mark.parametrize("test_file", test_files) +def test_save(test_file): + with Image.open(test_file) as im: + assert im.tell() == 0 + jpg0 = roundtrip(im) + assert_image_similar(im, jpg0, 30) + im.seek(1) + assert im.tell() == 1 + jpg1 = roundtrip(im) + assert_image_similar(im, jpg1, 30) def test_save_all(): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 0c8c9f30485..cbbb7df1d12 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -18,51 +18,48 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} -def test_sanity(tmp_path): - for mode in _MODES: - - def roundtrip(original_im): - out = str(tmp_path / "temp.tga") +@pytest.mark.parametrize("mode", _MODES) +def test_sanity(mode, tmp_path): + def roundtrip(original_im): + out = str(tmp_path / "temp.tga") - original_im.save(out, rle=rle) - with Image.open(out) as saved_im: - if rle: + original_im.save(out, rle=rle) + with Image.open(out) as saved_im: + if rle: + assert saved_im.info["compression"] == original_im.info["compression"] + assert saved_im.info["orientation"] == original_im.info["orientation"] + if mode == "P": + assert saved_im.getpalette() == original_im.getpalette() + + assert_image_equal(saved_im, original_im) + + png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) + + for png_path in png_paths: + with Image.open(png_path) as reference_im: + assert reference_im.mode == mode + + path_no_ext = os.path.splitext(png_path)[0] + for origin, rle in product(_ORIGINS, (True, False)): + tga_path = "{}_{}_{}.tga".format( + path_no_ext, origin, "rle" if rle else "raw" + ) + + with Image.open(tga_path) as original_im: + assert original_im.format == "TGA" + assert original_im.get_format_mimetype() == "image/x-tga" + if rle: + assert original_im.info["compression"] == "tga_rle" assert ( - saved_im.info["compression"] == original_im.info["compression"] + original_im.info["orientation"] + == _ORIGIN_TO_ORIENTATION[origin] ) - assert saved_im.info["orientation"] == original_im.info["orientation"] - if mode == "P": - assert saved_im.getpalette() == original_im.getpalette() - - assert_image_equal(saved_im, original_im) + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() - png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) - - for png_path in png_paths: - with Image.open(png_path) as reference_im: - assert reference_im.mode == mode - - path_no_ext = os.path.splitext(png_path)[0] - for origin, rle in product(_ORIGINS, (True, False)): - tga_path = "{}_{}_{}.tga".format( - path_no_ext, origin, "rle" if rle else "raw" - ) + assert_image_equal(original_im, reference_im) - with Image.open(tga_path) as original_im: - assert original_im.format == "TGA" - assert original_im.get_format_mimetype() == "image/x-tga" - if rle: - assert original_im.info["compression"] == "tga_rle" - assert ( - original_im.info["orientation"] - == _ORIGIN_TO_ORIENTATION[origin] - ) - if mode == "P": - assert original_im.getpalette() == reference_im.getpalette() - - assert_image_equal(original_im, reference_im) - - roundtrip(original_im) + roundtrip(original_im) def test_palette_depth_16(tmp_path): diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index d6769a24b0b..439cb15bca9 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -66,10 +66,10 @@ def test_load_set_dpi(): assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) -def test_save(tmp_path): +@pytest.mark.parametrize("ext", (".wmf", ".emf")) +def test_save(ext, tmp_path): im = hopper() - for ext in [".wmf", ".emf"]: - tmpfile = str(tmp_path / ("temp" + ext)) - with pytest.raises(OSError): - im.save(tmpfile) + tmpfile = str(tmp_path / ("temp" + ext)) + with pytest.raises(OSError): + im.save(tmpfile) diff --git a/Tests/test_image.py b/Tests/test_image.py index 6dc89918f05..7cebed127d9 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -22,8 +22,9 @@ class TestImage: - def test_image_modes_success(self): - for mode in [ + @pytest.mark.parametrize( + "mode", + ( "1", "P", "PA", @@ -44,22 +45,18 @@ def test_image_modes_success(self): "YCbCr", "LAB", "HSV", - ]: - Image.new(mode, (1, 1)) + ), + ) + def test_image_modes_success(self, mode): + Image.new(mode, (1, 1)) - def test_image_modes_fail(self): - for mode in [ - "", - "bad", - "very very long", - "BGR;15", - "BGR;16", - "BGR;24", - "BGR;32", - ]: - with pytest.raises(ValueError) as e: - Image.new(mode, (1, 1)) - assert str(e.value) == "unrecognized image mode" + @pytest.mark.parametrize( + "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32") + ) + def test_image_modes_fail(self, mode): + with pytest.raises(ValueError) as e: + Image.new(mode, (1, 1)) + assert str(e.value) == "unrecognized image mode" def test_exception_inheritance(self): assert issubclass(UnidentifiedImageError, OSError) @@ -539,23 +536,22 @@ def test_linear_gradient_wrong_mode(self): with pytest.raises(ValueError): Image.linear_gradient(wrong_mode) - def test_linear_gradient(self): - + @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) + def test_linear_gradient(self, mode): # Arrange target_file = "Tests/images/linear_gradient.png" - for mode in ["L", "P", "I", "F"]: - # Act - im = Image.linear_gradient(mode) + # Act + im = Image.linear_gradient(mode) - # Assert - assert im.size == (256, 256) - assert im.mode == mode - assert im.getpixel((0, 0)) == 0 - assert im.getpixel((255, 255)) == 255 - with Image.open(target_file) as target: - target = target.convert(mode) - assert_image_equal(im, target) + # Assert + assert im.size == (256, 256) + assert im.mode == mode + assert im.getpixel((0, 0)) == 0 + assert im.getpixel((255, 255)) == 255 + with Image.open(target_file) as target: + target = target.convert(mode) + assert_image_equal(im, target) def test_radial_gradient_wrong_mode(self): # Arrange @@ -565,23 +561,22 @@ def test_radial_gradient_wrong_mode(self): with pytest.raises(ValueError): Image.radial_gradient(wrong_mode) - def test_radial_gradient(self): - + @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) + def test_radial_gradient(self, mode): # Arrange target_file = "Tests/images/radial_gradient.png" - for mode in ["L", "P", "I", "F"]: - - # Act - im = Image.radial_gradient(mode) - - # Assert - assert im.size == (256, 256) - assert im.mode == mode - assert im.getpixel((0, 0)) == 255 - assert im.getpixel((128, 128)) == 0 - with Image.open(target_file) as target: - target = target.convert(mode) - assert_image_equal(im, target) + + # Act + im = Image.radial_gradient(mode) + + # Assert + assert im.size == (256, 256) + assert im.mode == mode + assert im.getpixel((0, 0)) == 255 + assert im.getpixel((128, 128)) == 0 + with Image.open(target_file) as target: + target = target.convert(mode) + assert_image_equal(im, target) def test_register_extensions(self): test_format = "a" diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 617274a576d..bb75eb0b5a9 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -184,8 +184,9 @@ def check(self, mode, c=None): with pytest.raises(error): im.getpixel((-1, -1)) - def test_basic(self): - for mode in ( + @pytest.mark.parametrize( + "mode", + ( "1", "L", "LA", @@ -200,23 +201,25 @@ def test_basic(self): "RGBX", "CMYK", "YCbCr", - ): - self.check(mode) + ), + ) + def test_basic(self, mode): + self.check(mode) - def test_signedness(self): + @pytest.mark.parametrize("mode", ("I;16", "I;16B")) + def test_signedness(self, mode): # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* - for mode in ("I;16", "I;16B"): - self.check(mode, 2**15 - 1) - self.check(mode, 2**15) - self.check(mode, 2**15 + 1) - self.check(mode, 2**16 - 1) - - def test_p_putpixel_rgb_rgba(self): - for color in [(255, 0, 0), (255, 0, 0, 255)]: - im = Image.new("P", (1, 1), 0) - im.putpixel((0, 0), color) - assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) + self.check(mode, 2**15 - 1) + self.check(mode, 2**15) + self.check(mode, 2**15 + 1) + self.check(mode, 2**16 - 1) + + @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) + def test_p_putpixel_rgb_rgba(self, color): + im = Image.new("P", (1, 1), 0) + im.putpixel((0, 0), color) + assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) @pytest.mark.skipif(cffi is None, reason="No CFFI") diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index e5639e10533..8f4b8b43c64 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -268,36 +268,33 @@ def test_matrix_wrong_mode(): im.convert(mode="L", matrix=matrix) -def test_matrix_xyz(): - def matrix_convert(mode): - # Arrange - im = hopper("RGB") - im.info["transparency"] = (255, 0, 0) - # fmt: off - matrix = ( - 0.412453, 0.357580, 0.180423, 0, - 0.212671, 0.715160, 0.072169, 0, - 0.019334, 0.119193, 0.950227, 0) - # fmt: on - assert im.mode == "RGB" - - # Act - # Convert an RGB image to the CIE XYZ colour space - converted_im = im.convert(mode=mode, matrix=matrix) - - # Assert - assert converted_im.mode == mode - assert converted_im.size == im.size - with Image.open("Tests/images/hopper-XYZ.png") as target: - if converted_im.mode == "RGB": - assert_image_similar(converted_im, target, 3) - assert converted_im.info["transparency"] == (105, 54, 4) - else: - assert_image_similar(converted_im, target.getchannel(0), 1) - assert converted_im.info["transparency"] == 105 - - matrix_convert("RGB") - matrix_convert("L") +@pytest.mark.parametrize("mode", ("RGB", "L")) +def test_matrix_xyz(mode): + # Arrange + im = hopper("RGB") + im.info["transparency"] = (255, 0, 0) + # fmt: off + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + # fmt: on + assert im.mode == "RGB" + + # Act + # Convert an RGB image to the CIE XYZ colour space + converted_im = im.convert(mode=mode, matrix=matrix) + + # Assert + assert converted_im.mode == mode + assert converted_im.size == im.size + with Image.open("Tests/images/hopper-XYZ.png") as target: + if converted_im.mode == "RGB": + assert_image_similar(converted_im, target, 3) + assert converted_im.info["transparency"] == (105, 54, 4) + else: + assert_image_similar(converted_im, target.getchannel(0), 1) + assert converted_im.info["transparency"] == 105 def test_matrix_identity(): diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 21e438654b1..591832147d7 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,37 +1,40 @@ import copy +import pytest + from PIL import Image from .helper import hopper -def test_copy(): +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_copy(mode): cropped_coordinates = (10, 10, 20, 20) cropped_size = (10, 10) - for mode in "1", "P", "L", "RGB", "I", "F": - # Internal copy method - im = hopper(mode) - out = im.copy() - assert out.mode == im.mode - assert out.size == im.size - - # Python's copy method - im = hopper(mode) - out = copy.copy(im) - assert out.mode == im.mode - assert out.size == im.size - - # Internal copy method on a cropped image - im = hopper(mode) - out = im.crop(cropped_coordinates).copy() - assert out.mode == im.mode - assert out.size == cropped_size - - # Python's copy method on a cropped image - im = hopper(mode) - out = copy.copy(im.crop(cropped_coordinates)) - assert out.mode == im.mode - assert out.size == cropped_size + + # Internal copy method + im = hopper(mode) + out = im.copy() + assert out.mode == im.mode + assert out.size == im.size + + # Python's copy method + im = hopper(mode) + out = copy.copy(im) + assert out.mode == im.mode + assert out.size == im.size + + # Internal copy method on a cropped image + im = hopper(mode) + out = im.crop(cropped_coordinates).copy() + assert out.mode == im.mode + assert out.size == cropped_size + + # Python's copy method on a cropped image + im = hopper(mode) + out = copy.copy(im.crop(cropped_coordinates)) + assert out.mode == im.mode + assert out.size == cropped_size def test_copy_zero(): diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 6574e6efd1a..4aa41de2792 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -5,17 +5,14 @@ from .helper import assert_image_equal, hopper -def test_crop(): - def crop(mode): - im = hopper(mode) - assert_image_equal(im.crop(), im) - - cropped = im.crop((50, 50, 100, 100)) - assert cropped.mode == mode - assert cropped.size == (50, 50) - - for mode in "1", "P", "L", "RGB", "I", "F": - crop(mode) +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_crop(mode): + im = hopper(mode) + assert_image_equal(im.crop(), im) + + cropped = im.crop((50, 50, 100, 100)) + assert cropped.mode == mode + assert cropped.size == (50, 50) def test_wide_crop(): diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 6d050efccba..883bb9b195f 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -100,40 +100,41 @@ def serialize_image(self, image): for y in range(image.size[1]) ) - def test_reduce_box(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.Resampling.BOX) - # fmt: off - data = ("e1 e1" - "e1 e1") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_reduce_bilinear(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.Resampling.BILINEAR) - # fmt: off - data = ("e1 c9" - "c9 b7") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_reduce_hamming(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.Resampling.HAMMING) - # fmt: off - data = ("e1 da" - "da d3") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_reduce_bicubic(self): + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_box(self, mode): + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.BOX) + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_bilinear(self, mode): + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.BILINEAR) + # fmt: off + data = ("e1 c9" + "c9 b7") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_hamming(self, mode): + case = self.make_case(mode, (8, 8), 0xE1) + case = case.resize((4, 4), Image.Resampling.HAMMING) + # fmt: off + data = ("e1 da" + "da d3") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_bicubic(self, mode): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.Resampling.BICUBIC) @@ -145,79 +146,79 @@ def test_reduce_bicubic(self): for channel in case.split(): self.check_case(channel, self.make_sample(data, (6, 6))) - def test_reduce_lanczos(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (16, 16), 0xE1) - case = case.resize((8, 8), Image.Resampling.LANCZOS) - # fmt: off - data = ("e1 e0 e4 d7" - "e0 df e3 d6" - "e4 e3 e7 da" - "d7 d6 d9 ce") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (8, 8))) - - def test_enlarge_box(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.Resampling.BOX) - # fmt: off - data = ("e1 e1" - "e1 e1") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_enlarge_bilinear(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.Resampling.BILINEAR) - # fmt: off - data = ("e1 b0" - "b0 98") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_enlarge_hamming(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.Resampling.HAMMING) - # fmt: off - data = ("e1 d2" - "d2 c5") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (4, 4))) - - def test_enlarge_bicubic(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (4, 4), 0xE1) - case = case.resize((8, 8), Image.Resampling.BICUBIC) - # fmt: off - data = ("e1 e5 ee b9" - "e5 e9 f3 bc" - "ee f3 fd c1" - "b9 bc c1 a2") - # fmt: on - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (8, 8))) - - def test_enlarge_lanczos(self): - for mode in ["RGBX", "RGB", "La", "L"]: - case = self.make_case(mode, (6, 6), 0xE1) - case = case.resize((12, 12), Image.Resampling.LANCZOS) - data = ( - "e1 e0 db ed f5 b8" - "e0 df da ec f3 b7" - "db db d6 e7 ee b5" - "ed ec e6 fb ff bf" - "f5 f4 ee ff ff c4" - "b8 b7 b4 bf c4 a0" - ) - for channel in case.split(): - self.check_case(channel, self.make_sample(data, (12, 12))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_lanczos(self, mode): + case = self.make_case(mode, (16, 16), 0xE1) + case = case.resize((8, 8), Image.Resampling.LANCZOS) + # fmt: off + data = ("e1 e0 e4 d7" + "e0 df e3 d6" + "e4 e3 e7 da" + "d7 d6 d9 ce") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_box(self, mode): + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.BOX) + # fmt: off + data = ("e1 e1" + "e1 e1") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_bilinear(self, mode): + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.BILINEAR) + # fmt: off + data = ("e1 b0" + "b0 98") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_hamming(self, mode): + case = self.make_case(mode, (2, 2), 0xE1) + case = case.resize((4, 4), Image.Resampling.HAMMING) + # fmt: off + data = ("e1 d2" + "d2 c5") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (4, 4))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_bicubic(self, mode): + case = self.make_case(mode, (4, 4), 0xE1) + case = case.resize((8, 8), Image.Resampling.BICUBIC) + # fmt: off + data = ("e1 e5 ee b9" + "e5 e9 f3 bc" + "ee f3 fd c1" + "b9 bc c1 a2") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_lanczos(self, mode): + case = self.make_case(mode, (6, 6), 0xE1) + case = case.resize((12, 12), Image.Resampling.LANCZOS) + data = ( + "e1 e0 db ed f5 b8" + "e0 df da ec f3 b7" + "db db d6 e7 ee b5" + "ed ec e6 fb ff bf" + "f5 f4 ee ff ff c4" + "b8 b7 b4 bf c4 a0" + ) + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (12, 12))) def test_box_filter_correct_range(self): im = Image.new("RGB", (8, 8), "#1688ff").resize( @@ -419,40 +420,43 @@ def test_nonzero_coefficients(self): class TestCoreResampleBox: - def test_wrong_arguments(self): - im = hopper() - for resample in ( + @pytest.mark.parametrize( + "resample", + ( Image.Resampling.NEAREST, Image.Resampling.BOX, Image.Resampling.BILINEAR, Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, - ): - im.resize((32, 32), resample, (0, 0, im.width, im.height)) - im.resize((32, 32), resample, (20, 20, im.width, im.height)) - im.resize((32, 32), resample, (20, 20, 20, 100)) - im.resize((32, 32), resample, (20, 20, 100, 20)) - - with pytest.raises(TypeError, match="must be sequence of length 4"): - im.resize((32, 32), resample, (im.width, im.height)) - - with pytest.raises(ValueError, match="can't be negative"): - im.resize((32, 32), resample, (-20, 20, 100, 100)) - with pytest.raises(ValueError, match="can't be negative"): - im.resize((32, 32), resample, (20, -20, 100, 100)) - - with pytest.raises(ValueError, match="can't be empty"): - im.resize((32, 32), resample, (20.1, 20, 20, 100)) - with pytest.raises(ValueError, match="can't be empty"): - im.resize((32, 32), resample, (20, 20.1, 100, 20)) - with pytest.raises(ValueError, match="can't be empty"): - im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) - - with pytest.raises(ValueError, match="can't exceed"): - im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) - with pytest.raises(ValueError, match="can't exceed"): - im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) + ), + ) + def test_wrong_arguments(self, resample): + im = hopper() + im.resize((32, 32), resample, (0, 0, im.width, im.height)) + im.resize((32, 32), resample, (20, 20, im.width, im.height)) + im.resize((32, 32), resample, (20, 20, 20, 100)) + im.resize((32, 32), resample, (20, 20, 100, 20)) + + with pytest.raises(TypeError, match="must be sequence of length 4"): + im.resize((32, 32), resample, (im.width, im.height)) + + with pytest.raises(ValueError, match="can't be negative"): + im.resize((32, 32), resample, (-20, 20, 100, 100)) + with pytest.raises(ValueError, match="can't be negative"): + im.resize((32, 32), resample, (20, -20, 100, 100)) + + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20.1, 20, 20, 100)) + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20, 20.1, 100, 20)) + with pytest.raises(ValueError, match="can't be empty"): + im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) + + with pytest.raises(ValueError, match="can't exceed"): + im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) + with pytest.raises(ValueError, match="can't exceed"): + im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) def resize_tiled(self, im, dst_size, xtiles, ytiles): def split_range(size, tiles): @@ -509,14 +513,14 @@ def test_subsample(self): with pytest.raises(AssertionError, match=r"difference 29\."): assert_image_similar(reference, without_box, 5) - def test_formats(self): + @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", "")) + def test_formats(self, mode): for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]: - for mode in ["RGB", "L", "RGBA", "LA", "I", ""]: - im = hopper(mode) - box = (20, 20, im.size[0] - 20, im.size[1] - 20) - with_box = im.resize((32, 32), resample, box) - cropped = im.crop(box).resize((32, 32), resample) - assert_image_similar(cropped, with_box, 0.4) + im = hopper(mode) + box = (20, 20, im.size[0] - 20, im.size[1] - 20) + with_box = im.resize((32, 32), resample, box) + cropped = im.crop(box).resize((32, 32), resample) + assert_image_similar(cropped, with_box, 0.4) def test_passthrough(self): # When no resize is required diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 8347fabb9e5..ae12202e4ed 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -22,24 +22,15 @@ def resize(self, im, size, f): im.load() return im._new(im.im.resize(size, f)) - def test_nearest_mode(self): - for mode in [ - "1", - "P", - "L", - "I", - "F", - "RGB", - "RGBA", - "CMYK", - "YCbCr", - "I;16", - ]: # exotic mode - im = hopper(mode) - r = self.resize(im, (15, 12), Image.Resampling.NEAREST) - assert r.mode == mode - assert r.size == (15, 12) - assert r.im.bands == im.im.bands + @pytest.mark.parametrize( + "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") + ) + def test_nearest_mode(self, mode): + im = hopper(mode) + r = self.resize(im, (15, 12), Image.Resampling.NEAREST) + assert r.mode == mode + assert r.size == (15, 12) + assert r.im.bands == im.im.bands def test_convolution_modes(self): with pytest.raises(ValueError): diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index f96864c53df..a19f19831fd 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import ( @@ -22,25 +24,25 @@ def rotate(im, mode, angle, center=None, translate=None): assert out.size != im.size -def test_mode(): - for mode in ("1", "P", "L", "RGB", "I", "F"): - im = hopper(mode) - rotate(im, mode, 45) +@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) +def test_mode(mode): + im = hopper(mode) + rotate(im, mode, 45) -def test_angle(): - for angle in (0, 90, 180, 270): - with Image.open("Tests/images/test-card.png") as im: - rotate(im, im.mode, angle) +@pytest.mark.parametrize("angle", (0, 90, 180, 270)) +def test_angle(angle): + with Image.open("Tests/images/test-card.png") as im: + rotate(im, im.mode, angle) - im = hopper() - assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) + im = hopper() + assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) -def test_zero(): - for angle in (0, 45, 90, 180, 270): - im = Image.new("RGB", (0, 0)) - rotate(im, im.mode, angle) +@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) +def test_zero(angle): + im = Image.new("RGB", (0, 0)) + rotate(im, im.mode, angle) def test_resample(): diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 6408e156491..877f439ca26 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,3 +1,5 @@ +import pytest + from PIL.Image import Transpose from . import helper @@ -9,157 +11,136 @@ } -def test_flip_left_right(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.FLIP_LEFT_RIGHT) - assert out.mode == mode - assert out.size == im.size - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) - assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) - - for mode in HOPPER: - transpose(mode) - - -def test_flip_top_bottom(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.FLIP_TOP_BOTTOM) - assert out.mode == mode - assert out.size == im.size - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) - - for mode in HOPPER: - transpose(mode) - - -def test_rotate_90(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.ROTATE_90) - assert out.mode == mode - assert out.size == im.size[::-1] - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) - assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) - - for mode in HOPPER: - transpose(mode) - - -def test_rotate_180(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.ROTATE_180) - assert out.mode == mode - assert out.size == im.size - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) - - for mode in HOPPER: - transpose(mode) - - -def test_rotate_270(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.ROTATE_270) - assert out.mode == mode - assert out.size == im.size[::-1] - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) - assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) - - for mode in HOPPER: - transpose(mode) - - -def test_transpose(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.TRANSPOSE) - assert out.mode == mode - assert out.size == im.size[::-1] - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((1, 1)) - assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) - assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) - - for mode in HOPPER: - transpose(mode) - - -def test_tranverse(): - def transpose(mode): - im = HOPPER[mode] - out = im.transpose(Transpose.TRANSVERSE) - assert out.mode == mode - assert out.size == im.size[::-1] - - x, y = im.size - assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) - assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) - assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) - assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) - - for mode in HOPPER: - transpose(mode) - - -def test_roundtrip(): - for mode in HOPPER: - im = HOPPER[mode] - - def transpose(first, second): - return im.transpose(first).transpose(second) - - assert_image_equal( - im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) - ) - assert_image_equal( - im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) - ) - assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) - assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) - assert_image_equal( - im.transpose(Transpose.TRANSPOSE), - transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), - ) - assert_image_equal( - im.transpose(Transpose.TRANSPOSE), - transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), - ) - assert_image_equal( - im.transpose(Transpose.TRANSVERSE), - transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), - ) - assert_image_equal( - im.transpose(Transpose.TRANSVERSE), - transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), - ) - assert_image_equal( - im.transpose(Transpose.TRANSVERSE), - transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), - ) +@pytest.mark.parametrize("mode", HOPPER) +def test_flip_left_right(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.FLIP_LEFT_RIGHT) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_flip_top_bottom(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.FLIP_TOP_BOTTOM) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_90(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_90) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_180(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_180) + assert out.mode == mode + assert out.size == im.size + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_rotate_270(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.ROTATE_270) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_transpose(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.TRANSPOSE) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((1, 1)) + assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) + assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_tranverse(mode): + im = HOPPER[mode] + out = im.transpose(Transpose.TRANSVERSE) + assert out.mode == mode + assert out.size == im.size[::-1] + + x, y = im.size + assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) + assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) + assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) + assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) + + +@pytest.mark.parametrize("mode", HOPPER) +def test_roundtrip(mode): + im = HOPPER[mode] + + def transpose(first, second): + return im.transpose(first).transpose(second) + + assert_image_equal( + im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) + ) + assert_image_equal( + im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) + ) + assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) + assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) + assert_image_equal( + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), + ) + assert_image_equal( + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), + ) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 961b4d08130..d1dd1e47c1c 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -625,20 +625,20 @@ def test_polygon2(): helper_polygon(POINTS2) -def test_polygon_kite(): +@pytest.mark.parametrize("mode", ("RGB", "L")) +def test_polygon_kite(mode): # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines - for mode in ["RGB", "L"]: - # Arrange - im = Image.new(mode, (W, H)) - draw = ImageDraw.Draw(im) - expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" - # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + # Act + draw.polygon(KITE_POINTS, fill="blue", outline="yellow") - # Assert - assert_image_equal_tofile(im, expected) + # Assert + assert_image_equal_tofile(im, expected) def test_polygon_1px_high(): diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 60bfaeb9b75..af0b0c2935f 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -16,32 +16,32 @@ from PIL.ImageQt import QImage -def test_sanity(tmp_path): - for mode in ("RGB", "RGBA", "L", "P", "1"): - src = hopper(mode) - data = ImageQt.toqimage(src) - - assert isinstance(data, QImage) - assert not data.isNull() - - # reload directly from the qimage - rt = ImageQt.fromqimage(data) - if mode in ("L", "P", "1"): - assert_image_equal(rt, src.convert("RGB")) - else: - assert_image_equal(rt, src) - - if mode == "1": - # BW appears to not save correctly on QT4 and QT5 - # kicks out errors on console: - # libpng warning: Invalid color type/bit depth combination - # in IHDR - # libpng error: Invalid IHDR data - continue - - # Test saving the file - tempfile = str(tmp_path / f"temp_{mode}.png") - data.save(tempfile) - - # Check that it actually worked. - assert_image_equal_tofile(src, tempfile) +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) +def test_sanity(mode, tmp_path): + src = hopper(mode) + data = ImageQt.toqimage(src) + + assert isinstance(data, QImage) + assert not data.isNull() + + # reload directly from the qimage + rt = ImageQt.fromqimage(data) + if mode in ("L", "P", "1"): + assert_image_equal(rt, src.convert("RGB")) + else: + assert_image_equal(rt, src) + + if mode == "1": + # BW appears to not save correctly on QT4 and QT5 + # kicks out errors on console: + # libpng warning: Invalid color type/bit depth combination + # in IHDR + # libpng error: Invalid IHDR data + return + + # Test saving the file + tempfile = str(tmp_path / f"temp_{mode}.png") + data.save(tempfile) + + # Check that it actually worked. + assert_image_equal_tofile(src, tempfile) From 1c391fe31f902b604a7bc4ebd9b4315fa5ef8e1f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 08:11:02 +1000 Subject: [PATCH 155/242] Renamed argument --- Tests/test_file_apng.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index d624bbb849c..0ff05f608c2 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -326,7 +326,7 @@ def open(): @pytest.mark.parametrize( - "f", + "test_file", ( "sequence_start.png", "sequence_gap.png", @@ -337,9 +337,9 @@ def open(): "sequence_fdat_fctl.png", ), ) -def test_apng_sequence_errors(f): +def test_apng_sequence_errors(test_file): with pytest.raises(SyntaxError): - with Image.open(f"Tests/images/apng/{f}") as im: + with Image.open(f"Tests/images/apng/{test_file}") as im: im.seek(im.n_frames - 1) im.load() From 8f25ea46ebd471c48eb424c8754ea1747a54776a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 24 Aug 2022 08:12:14 +1000 Subject: [PATCH 156/242] Qt4 is no longer supported Co-authored-by: Hugo van Kemenade --- Tests/test_qt_image_toqimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index af0b0c2935f..c1983031a14 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -32,7 +32,7 @@ def test_sanity(mode, tmp_path): assert_image_equal(rt, src) if mode == "1": - # BW appears to not save correctly on QT4 and QT5 + # BW appears to not save correctly on QT5 # kicks out errors on console: # libpng warning: Invalid color type/bit depth combination # in IHDR From 3353ea80e1c873acdb11636cf3d387b8e59580c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 10:37:40 +1000 Subject: [PATCH 157/242] Further parametrizations --- Tests/test_image_resample.py | 16 ++- Tests/test_image_resize.py | 241 ++++++++++++++++++----------------- 2 files changed, 135 insertions(+), 122 deletions(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 883bb9b195f..5ce98a23568 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -514,13 +514,15 @@ def test_subsample(self): assert_image_similar(reference, without_box, 5) @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", "")) - def test_formats(self, mode): - for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]: - im = hopper(mode) - box = (20, 20, im.size[0] - 20, im.size[1] - 20) - with_box = im.resize((32, 32), resample, box) - cropped = im.crop(box).resize((32, 32), resample) - assert_image_similar(cropped, with_box, 0.4) + @pytest.mark.parametrize( + "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) + ) + def test_formats(self, mode, resample): + im = hopper(mode) + box = (20, 20, im.size[0] - 20, im.size[1] - 20) + with_box = im.resize((32, 32), resample, box) + cropped = im.crop(box).resize((32, 32), resample) + assert_image_similar(cropped, with_box, 0.4) def test_passthrough(self): # When no resize is required diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index ae12202e4ed..83c54cf6211 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -46,33 +46,58 @@ def test_convolution_modes(self): assert r.size == (15, 12) assert r.im.bands == im.im.bands - def test_reduce_filters(self): - for f in [ + @pytest.mark.parametrize( + "resample", + ( Image.Resampling.NEAREST, Image.Resampling.BOX, Image.Resampling.BILINEAR, Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, - ]: - r = self.resize(hopper("RGB"), (15, 12), f) - assert r.mode == "RGB" - assert r.size == (15, 12) + ), + ) + def test_reduce_filters(self, resample): + r = self.resize(hopper("RGB"), (15, 12), resample) + assert r.mode == "RGB" + assert r.size == (15, 12) - def test_enlarge_filters(self): - for f in [ + @pytest.mark.parametrize( + "resample", + ( Image.Resampling.NEAREST, Image.Resampling.BOX, Image.Resampling.BILINEAR, Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, - ]: - r = self.resize(hopper("RGB"), (212, 195), f) - assert r.mode == "RGB" - assert r.size == (212, 195) + ), + ) + def test_enlarge_filters(self, resample): + r = self.resize(hopper("RGB"), (212, 195), resample) + assert r.mode == "RGB" + assert r.size == (212, 195) - def test_endianness(self): + @pytest.mark.parametrize( + "resample", + ( + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, + ), + ) + @pytest.mark.parametrize( + "mode, channels_set", + ( + ("RGB", ("blank", "filled", "dirty")), + ("RGBA", ("blank", "blank", "filled", "dirty")), + ("LA", ("filled", "dirty")), + ), + ) + def test_endianness(self, resample, mode, channels_set): # Make an image with one colored pixel, in one channel. # When resized, that channel should be the same as a GS image. # Other channels should be unaffected. @@ -86,47 +111,37 @@ def test_endianness(self): } samples["dirty"].putpixel((1, 1), 128) - for f in [ - Image.Resampling.NEAREST, - Image.Resampling.BOX, - Image.Resampling.BILINEAR, - Image.Resampling.HAMMING, - Image.Resampling.BICUBIC, - Image.Resampling.LANCZOS, - ]: - # samples resized with current filter - references = { - name: self.resize(ch, (4, 4), f) for name, ch in samples.items() - } - - for mode, channels_set in [ - ("RGB", ("blank", "filled", "dirty")), - ("RGBA", ("blank", "blank", "filled", "dirty")), - ("LA", ("filled", "dirty")), - ]: - for channels in set(permutations(channels_set)): - # compile image from different channels permutations - im = Image.merge(mode, [samples[ch] for ch in channels]) - resized = self.resize(im, (4, 4), f) - - for i, ch in enumerate(resized.split()): - # check what resized channel in image is the same - # as separately resized channel - assert_image_equal(ch, references[channels[i]]) - - def test_enlarge_zero(self): - for f in [ + # samples resized with current filter + references = { + name: self.resize(ch, (4, 4), resample) for name, ch in samples.items() + } + + for channels in set(permutations(channels_set)): + # compile image from different channels permutations + im = Image.merge(mode, [samples[ch] for ch in channels]) + resized = self.resize(im, (4, 4), resample) + + for i, ch in enumerate(resized.split()): + # check what resized channel in image is the same + # as separately resized channel + assert_image_equal(ch, references[channels[i]]) + + @pytest.mark.parametrize( + "resample", + ( Image.Resampling.NEAREST, Image.Resampling.BOX, Image.Resampling.BILINEAR, Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, - ]: - r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f) - assert r.mode == "RGB" - assert r.size == (212, 195) - assert r.getdata()[0] == (0, 0, 0) + ), + ) + def test_enlarge_zero(self, resample): + r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) + assert r.mode == "RGB" + assert r.size == (212, 195) + assert r.getdata()[0] == (0, 0, 0) def test_unknown_filter(self): with pytest.raises(ValueError): @@ -170,74 +185,71 @@ def test_reducing_gap_values(self, gradients_image): (52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99 ) - def test_reducing_gap_1(self, gradients_image): - for box, epsilon in [ - (None, 4), - ((1.1, 2.2, 510.8, 510.9), 4), - ((3, 10, 410, 256), 10), - ]: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 - ) + @pytest.mark.parametrize( + "box, epsilon", + ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), + ) + def test_reducing_gap_1(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 + ) - with pytest.raises(AssertionError): - assert_image_equal(ref, im) + with pytest.raises(AssertionError): + assert_image_equal(ref, im) - assert_image_similar(ref, im, epsilon) + assert_image_similar(ref, im, epsilon) - def test_reducing_gap_2(self, gradients_image): - for box, epsilon in [ - (None, 1.5), - ((1.1, 2.2, 510.8, 510.9), 1.5), - ((3, 10, 410, 256), 1), - ]: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 - ) + @pytest.mark.parametrize( + "box, epsilon", + ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), + ) + def test_reducing_gap_2(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 + ) - with pytest.raises(AssertionError): - assert_image_equal(ref, im) + with pytest.raises(AssertionError): + assert_image_equal(ref, im) - assert_image_similar(ref, im, epsilon) + assert_image_similar(ref, im, epsilon) - def test_reducing_gap_3(self, gradients_image): - for box, epsilon in [ - (None, 1), - ((1.1, 2.2, 510.8, 510.9), 1), - ((3, 10, 410, 256), 0.5), - ]: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 - ) + @pytest.mark.parametrize( + "box, epsilon", + ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), + ) + def test_reducing_gap_3(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 + ) - with pytest.raises(AssertionError): - assert_image_equal(ref, im) + with pytest.raises(AssertionError): + assert_image_equal(ref, im) - assert_image_similar(ref, im, epsilon) + assert_image_similar(ref, im, epsilon) - def test_reducing_gap_8(self, gradients_image): - for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]: - ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 - ) + @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) + def test_reducing_gap_8(self, gradients_image, box): + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 + ) - assert_image_equal(ref, im) + assert_image_equal(ref, im) - def test_box_filter(self, gradients_image): - for box, epsilon in [ - ((0, 0, 512, 512), 5.5), - ((0.9, 1.7, 128, 128), 9.5), - ]: - ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) - im = gradients_image.resize( - (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 - ) + @pytest.mark.parametrize( + "box, epsilon", + (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), + ) + def test_box_filter(self, gradients_image, box, epsilon): + ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 + ) - assert_image_similar(ref, im, epsilon) + assert_image_similar(ref, im, epsilon) class TestImageResize: @@ -264,15 +276,14 @@ def test_load_first(self): im = im.resize((64, 64)) assert im.size == (64, 64) - def test_default_filter(self): - for mode in "L", "RGB", "I", "F": - im = hopper(mode) - assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) - - for mode in "1", "P": - im = hopper(mode) - assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) + @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) + def test_default_filter_bicubic(self, mode): + im = hopper(mode) + assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) - for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16": - im = hopper(mode) - assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) + @pytest.mark.parametrize( + "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") + ) + def test_default_filter_nearest(self, mode): + im = hopper(mode) + assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) From 56ba3ff68c678d0bf5f483b08f8c7428009ac226 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 15:39:43 +1000 Subject: [PATCH 158/242] Build lcms2 VC2022 --- winbuild/build_prepare.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a381d636dd8..94e5dd87114 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -226,21 +226,21 @@ def cmd_msbuild( "filename": "lcms2-2.13.1.tar.gz", "dir": "lcms2-2.13.1", "patch": { - r"Projects\VC2019\lcms2_static\lcms2_static.vcxproj": { + r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { # default is /MD for x86 and /MT for x64, we need /MD always "MultiThreaded": "MultiThreadedDLL", # noqa: E501 # retarget to default toolset (selected by vcvarsall.bat) - "v142": "$(DefaultPlatformToolset)", # noqa: E501 + "v143": "$(DefaultPlatformToolset)", # noqa: E501 # retarget to latest (selected by vcvarsall.bat) "10.0": "$(WindowsSDKVersion)", # noqa: E501 } }, "build": [ cmd_rmdir("Lib"), - cmd_rmdir(r"Projects\VC2019\Release"), - cmd_msbuild(r"Projects\VC2019\lcms2.sln", "Release", "Clean"), + cmd_rmdir(r"Projects\VC2022\Release"), + cmd_msbuild(r"Projects\VC2022\lcms2.sln", "Release", "Clean"), cmd_msbuild( - r"Projects\VC2019\lcms2.sln", "Release", "lcms2_static:Rebuild" + r"Projects\VC2022\lcms2.sln", "Release", "lcms2_static:Rebuild" ), cmd_xcopy("include", "{inc_dir}"), ], From df4bb3460000d222b3ac077dad925c32093f6b32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 22:32:42 +1000 Subject: [PATCH 159/242] Added test --- Tests/test_imageops.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 01e40e6d4d5..e3d4136517f 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -110,6 +110,16 @@ def test_contain(new_size): assert new_im.size == (256, 256) +def test_contain_round(): + im = Image.new("1", (43, 63), 1) + new_im = ImageOps.contain(im, (5, 7)) + assert new_im.width == 5 + + im = Image.new("1", (63, 43), 1) + new_im = ImageOps.contain(im, (7, 5)) + assert new_im.height == 5 + + def test_pad(): # Same ratio im = hopper() From f0be6845f7bff340aaf07bea9f4ded35a28f96fa Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 07:42:51 -0500 Subject: [PATCH 160/242] parametrize tests --- Tests/test_image_filter.py | 190 ++++++++++++++++++++----------------- 1 file changed, 105 insertions(+), 85 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 14a8da9f102..e12e73f9774 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -5,90 +5,110 @@ from .helper import assert_image_equal, hopper -def test_sanity(): - def apply_filter(filter_to_apply): - for mode in ["L", "RGB", "CMYK"]: - im = hopper(mode) - out = im.filter(filter_to_apply) - assert out.mode == im.mode - assert out.size == im.size - - apply_filter(ImageFilter.BLUR) - apply_filter(ImageFilter.CONTOUR) - apply_filter(ImageFilter.DETAIL) - apply_filter(ImageFilter.EDGE_ENHANCE) - apply_filter(ImageFilter.EDGE_ENHANCE_MORE) - apply_filter(ImageFilter.EMBOSS) - apply_filter(ImageFilter.FIND_EDGES) - apply_filter(ImageFilter.SMOOTH) - apply_filter(ImageFilter.SMOOTH_MORE) - apply_filter(ImageFilter.SHARPEN) - apply_filter(ImageFilter.MaxFilter) - apply_filter(ImageFilter.MedianFilter) - apply_filter(ImageFilter.MinFilter) - apply_filter(ImageFilter.ModeFilter) - apply_filter(ImageFilter.GaussianBlur) - apply_filter(ImageFilter.GaussianBlur(5)) - apply_filter(ImageFilter.BoxBlur(5)) - apply_filter(ImageFilter.UnsharpMask) - apply_filter(ImageFilter.UnsharpMask(10)) - +@pytest.mark.parametrize( + "filter_to_apply", + ( + ImageFilter.BLUR, + ImageFilter.CONTOUR, + ImageFilter.DETAIL, + ImageFilter.EDGE_ENHANCE, + ImageFilter.EDGE_ENHANCE_MORE, + ImageFilter.EMBOSS, + ImageFilter.FIND_EDGES, + ImageFilter.SMOOTH, + ImageFilter.SMOOTH_MORE, + ImageFilter.SHARPEN, + ImageFilter.MaxFilter, + ImageFilter.MedianFilter, + ImageFilter.MinFilter, + ImageFilter.ModeFilter, + ImageFilter.GaussianBlur, + ImageFilter.GaussianBlur(5), + ImageFilter.BoxBlur(5), + ImageFilter.UnsharpMask, + ImageFilter.UnsharpMask(10), + ), +) +@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +def test_sanity(filter_to_apply, mode): + im = hopper(mode) + out = im.filter(filter_to_apply) + assert out.mode == im.mode + assert out.size == im.size + + +@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) +def test_sanity_error(mode): with pytest.raises(TypeError): - apply_filter("hello") - - -def test_crash(): - - # crashes on small images - im = Image.new("RGB", (1, 1)) - im.filter(ImageFilter.SMOOTH) + im = hopper(mode) + out = im.filter("hello") + assert out.mode == im.mode + assert out.size == im.size - im = Image.new("RGB", (2, 2)) - im.filter(ImageFilter.SMOOTH) - im = Image.new("RGB", (3, 3)) +# crashes on small images +@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) +def test_crash(size): + im = Image.new("RGB", size) im.filter(ImageFilter.SMOOTH) -def test_modefilter(): - def modefilter(mode): - im = Image.new(mode, (3, 3), None) - im.putdata(list(range(9))) - # image is: - # 0 1 2 - # 3 4 5 - # 6 7 8 - mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 - mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) - return mod, mod2 - - assert modefilter("1") == (4, 0) - assert modefilter("L") == (4, 0) - assert modefilter("P") == (4, 0) - assert modefilter("RGB") == ((4, 0, 0), (0, 0, 0)) - - -def test_rankfilter(): - def rankfilter(mode): - im = Image.new(mode, (3, 3), None) +@pytest.mark.parametrize( + "mode,expected", + ( + ("1", (4, 0)), + ("L", (4, 0)), + ("P", (4, 0)), + ("RGB", ((4, 0, 0), (0, 0, 0))), + ), +) +def test_modefilter(mode, expected): + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 + mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) + assert (mod, mod2) == expected + + +@pytest.mark.parametrize( + "mode,expected", + ( + ("1", (0, 4, 8)), + ("L", (0, 4, 8)), + ("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))), + ("I", (0, 4, 8)), + ("F", (0.0, 4.0, 8.0)), + ), +) +def test_rankfilter(mode, expected): + im = Image.new(mode, (3, 3), None) + im.putdata(list(range(9))) + # image is: + # 0 1 2 + # 3 4 5 + # 6 7 8 + minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) + med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) + maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) + assert (minimum, med, maximum) == expected + + +def test_rankfilter_error(): + with pytest.raises(ValueError): + im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) # image is: # 0 1 2 # 3 4 5 # 6 7 8 - minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) - med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) - maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) - return minimum, med, maximum - - assert rankfilter("1") == (0, 4, 8) - assert rankfilter("L") == (0, 4, 8) - with pytest.raises(ValueError): - rankfilter("P") - assert rankfilter("RGB") == ((0, 0, 0), (4, 0, 0), (8, 0, 0)) - assert rankfilter("I") == (0, 4, 8) - assert rankfilter("F") == (0.0, 4.0, 8.0) + im.filter(ImageFilter.MinFilter).getpixel((1, 1)) + im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) + im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) def test_rankfilter_properties(): @@ -110,7 +130,8 @@ def test_kernel_not_enough_coefficients(): ImageFilter.Kernel((3, 3), (0, 0)) -def test_consistency_3x3(): +@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +def test_consistency_3x3(mode): with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper_emboss.bmp") as reference: kernel = ImageFilter.Kernel( @@ -125,14 +146,14 @@ def test_consistency_3x3(): source = source.split() * 2 reference = reference.split() * 2 - for mode in ["L", "LA", "RGB", "CMYK"]: - assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), - ) + assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), + ) -def test_consistency_5x5(): +@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) +def test_consistency_5x5(mode): with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: kernel = ImageFilter.Kernel( @@ -149,8 +170,7 @@ def test_consistency_5x5(): source = source.split() * 2 reference = reference.split() * 2 - for mode in ["L", "LA", "RGB", "CMYK"]: - assert_image_equal( - Image.merge(mode, source[: len(mode)]).filter(kernel), - Image.merge(mode, reference[: len(mode)]), - ) + assert_image_equal( + Image.merge(mode, source[: len(mode)]).filter(kernel), + Image.merge(mode, reference[: len(mode)]), + ) From fa591e11987d846ae726efd81ab5b743675e170e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 07:43:31 -0500 Subject: [PATCH 161/242] parametrize tests --- Tests/test_image_reduce.py | 160 ++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 74 deletions(-) diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index 70dc87f0a86..90beeeb689c 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -38,58 +38,64 @@ gradients_image.load() -def test_args_factor(): +@pytest.mark.parametrize( + "size,expected", + ( + (3, (4, 4)), + ((3, 1), (4, 10)), + ((1, 3), (10, 4)), + ), +) +def test_args_factor(size, expected): im = Image.new("L", (10, 10)) + assert expected == im.reduce(size).size - assert (4, 4) == im.reduce(3).size - assert (4, 10) == im.reduce((3, 1)).size - assert (10, 4) == im.reduce((1, 3)).size - with pytest.raises(ValueError): - im.reduce(0) - with pytest.raises(TypeError): - im.reduce(2.0) - with pytest.raises(ValueError): - im.reduce((0, 10)) - - -def test_args_box(): +@pytest.mark.parametrize( + "size,error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) +) +def test_args_factor_error(size, error): im = Image.new("L", (10, 10)) - - assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size - assert (1, 1) == im.reduce(2, (5, 5, 6, 6)).size - - with pytest.raises(TypeError): - im.reduce(2, "stri") - with pytest.raises(TypeError): - im.reduce(2, 2) - with pytest.raises(ValueError): - im.reduce(2, (0, 0, 11, 10)) - with pytest.raises(ValueError): - im.reduce(2, (0, 0, 10, 11)) - with pytest.raises(ValueError): - im.reduce(2, (-1, 0, 10, 10)) - with pytest.raises(ValueError): - im.reduce(2, (0, -1, 10, 10)) - with pytest.raises(ValueError): - im.reduce(2, (0, 5, 10, 5)) - with pytest.raises(ValueError): - im.reduce(2, (5, 0, 5, 10)) + with pytest.raises(error): + im.reduce(size) + + +@pytest.mark.parametrize( + "size,expected", + ( + ((0, 0, 10, 10), (5, 5)), + ((5, 5, 6, 6), (1, 1)), + ), +) +def test_args_box(size, expected): + im = Image.new("L", (10, 10)) + assert expected == im.reduce(2, size).size + + +@pytest.mark.parametrize( + "size,error", + ( + ("stri", TypeError), + ((0, 0, 11, 10), ValueError), + ((0, 0, 10, 11), ValueError), + ((-1, 0, 10, 10), ValueError), + ((0, -1, 10, 10), ValueError), + ((0, 5, 10, 5), ValueError), + ((5, 0, 5, 10), ValueError), + ), +) +def test_args_box_error(size, error): + im = Image.new("L", (10, 10)) + with pytest.raises(error): + im.reduce(2, size).size -def test_unsupported_modes(): +@pytest.mark.parametrize("mode", ("P", "1", "I;16")) +def test_unsupported_modes(mode): im = Image.new("P", (10, 10)) with pytest.raises(ValueError): im.reduce(3) - im = Image.new("1", (10, 10)) - with pytest.raises(ValueError): - im.reduce(3) - - im = Image.new("I;16", (10, 10)) - with pytest.raises(ValueError): - im.reduce(3) - def get_image(mode): mode_info = ImageMode.getmode(mode) @@ -197,63 +203,69 @@ def test_mode_L(): compare_reduce_with_box(im, factor) -def test_mode_LA(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_LA(factor): im = get_image("LA") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor, 0.8, 5) + compare_reduce_with_reference(im, factor, 0.8, 5) + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_LA_opaque(factor): + im = get_image("LA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_La(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_La(factor): im = get_image("La") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_RGB(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGB(factor): im = get_image("RGB") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_RGBA(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBA(factor): im = get_image("RGBA") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor, 0.8, 5) + compare_reduce_with_reference(im, factor, 0.8, 5) + +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBA_opaque(factor): + im = get_image("RGBA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_RGBa(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_RGBa(factor): im = get_image("RGBa") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_I(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_I(factor): im = get_image("I") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) -def test_mode_F(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_F(factor): im = get_image("F") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor, 0, 0) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor, 0, 0) + compare_reduce_with_box(im, factor) @skip_unless_feature("jpg_2000") From a7f7f6ac054a15e6f88a8b8724017d3ff1ff134c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 07:43:49 -0500 Subject: [PATCH 162/242] parametrize tests --- Tests/test_image_transform.py | 159 +++++++++++++++++----------------- 1 file changed, 78 insertions(+), 81 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index ac0e74969b0..14ca0334aa9 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -75,23 +75,25 @@ def test_quad(self): assert_image_equal(transformed, scaled) - def test_fill(self): - for mode, pixel in [ - ["RGB", (255, 0, 0)], - ["RGBA", (255, 0, 0, 255)], - ["LA", (76, 0)], - ]: - im = hopper(mode) - (w, h) = im.size - transformed = im.transform( - im.size, - Image.Transform.EXTENT, - (0, 0, w * 2, h * 2), - Image.Resampling.BILINEAR, - fillcolor="red", - ) - - assert transformed.getpixel((w - 1, h - 1)) == pixel + @pytest.mark.parametrize( + "mode,pixel", + ( + ("RGB", (255, 0, 0)), + ("RGBA", (255, 0, 0, 255)), + ("LA", (76, 0)), + ), + ) + def test_fill(self, mode, pixel): + im = hopper(mode) + (w, h) = im.size + transformed = im.transform( + im.size, + Image.Transform.EXTENT, + (0, 0, w * 2, h * 2), + Image.Resampling.BILINEAR, + fillcolor="red", + ) + assert transformed.getpixel((w - 1, h - 1)) == pixel def test_mesh(self): # this should be a checkerboard of halfsized hoppers in ul, lr @@ -222,14 +224,12 @@ def test_missing_method_data(self): with pytest.raises(ValueError): im.transform((100, 100), None) - def test_unknown_resampling_filter(self): + @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) + def test_unknown_resampling_filter(self, resample): with hopper() as im: (w, h) = im.size - for resample in (Image.Resampling.BOX, "unknown"): - with pytest.raises(ValueError): - im.transform( - (100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample - ) + with pytest.raises(ValueError): + im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) class TestImageTransformAffine: @@ -239,7 +239,16 @@ def _test_image(self): im = hopper("RGB") return im.crop((10, 20, im.width - 10, im.height - 20)) - def _test_rotate(self, deg, transpose): + @pytest.mark.parametrize( + "deg,transpose", + ( + (0, None), + (90, Image.Transpose.ROTATE_90), + (180, Image.Transpose.ROTATE_180), + (270, Image.Transpose.ROTATE_270), + ), + ) + def test_rotate(self, deg, transpose): im = self._test_image() angle = -math.radians(deg) @@ -271,77 +280,65 @@ def _test_rotate(self, deg, transpose): ) assert_image_equal(transposed, transformed) - def test_rotate_0_deg(self): - self._test_rotate(0, None) - - def test_rotate_90_deg(self): - self._test_rotate(90, Image.Transpose.ROTATE_90) - - def test_rotate_180_deg(self): - self._test_rotate(180, Image.Transpose.ROTATE_180) - - def test_rotate_270_deg(self): - self._test_rotate(270, Image.Transpose.ROTATE_270) - - def _test_resize(self, scale, epsilonscale): + @pytest.mark.parametrize( + "scale,epsilonscale", + ( + (1.1, 6.9), + (1.5, 5.5), + (2.0, 5.5), + (2.3, 3.7), + (2.5, 3.7), + ), + ) + @pytest.mark.parametrize( + "resample,epsilon", + ( + (Image.Resampling.NEAREST, 0), + (Image.Resampling.BILINEAR, 2), + (Image.Resampling.BICUBIC, 1), + ), + ) + def test_resize(self, scale, epsilonscale, resample, epsilon): im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0] matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] - for resample, epsilon in [ + transformed = im.transform(size_up, self.transform, matrix_up, resample) + transformed = transformed.transform( + im.size, self.transform, matrix_down, resample + ) + assert_image_similar(transformed, im, epsilon * epsilonscale) + + @pytest.mark.parametrize( + "x,y,epsilonscale", + ( + (0.1, 0, 3.7), + (0.6, 0, 9.1), + (50, 50, 0), + ), + ) + @pytest.mark.parametrize( + "resample,epsilon", + ( (Image.Resampling.NEAREST, 0), - (Image.Resampling.BILINEAR, 2), + (Image.Resampling.BILINEAR, 1.5), (Image.Resampling.BICUBIC, 1), - ]: - transformed = im.transform(size_up, self.transform, matrix_up, resample) - transformed = transformed.transform( - im.size, self.transform, matrix_down, resample - ) - assert_image_similar(transformed, im, epsilon * epsilonscale) - - def test_resize_1_1x(self): - self._test_resize(1.1, 6.9) - - def test_resize_1_5x(self): - self._test_resize(1.5, 5.5) - - def test_resize_2_0x(self): - self._test_resize(2.0, 5.5) - - def test_resize_2_3x(self): - self._test_resize(2.3, 3.7) - - def test_resize_2_5x(self): - self._test_resize(2.5, 3.7) - - def _test_translate(self, x, y, epsilonscale): + ), + ) + def test_translate(self, x, y, epsilonscale, resample, epsilon): im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) matrix_up = [1, 0, -x, 0, 1, -y, 0, 0] matrix_down = [1, 0, x, 0, 1, y, 0, 0] - for resample, epsilon in [ - (Image.Resampling.NEAREST, 0), - (Image.Resampling.BILINEAR, 1.5), - (Image.Resampling.BICUBIC, 1), - ]: - transformed = im.transform(size_up, self.transform, matrix_up, resample) - transformed = transformed.transform( - im.size, self.transform, matrix_down, resample - ) - assert_image_similar(transformed, im, epsilon * epsilonscale) - - def test_translate_0_1(self): - self._test_translate(0.1, 0, 3.7) - - def test_translate_0_6(self): - self._test_translate(0.6, 0, 9.1) - - def test_translate_50(self): - self._test_translate(50, 50, 0) + transformed = im.transform(size_up, self.transform, matrix_up, resample) + transformed = transformed.transform( + im.size, self.transform, matrix_down, resample + ) + assert_image_similar(transformed, im, epsilon * epsilonscale) class TestImageTransformPerspective(TestImageTransformAffine): From f9d3ee0f4888f7618071c0a5315c916062e78854 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Aug 2022 22:56:19 +1000 Subject: [PATCH 163/242] Round position in pad() --- Tests/test_imageops.py | 9 +++++++++ src/PIL/ImageOps.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index e3d4136517f..550578f8f7a 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -140,6 +140,15 @@ def test_pad(): ) +def test_pad_round(): + im = Image.new("1", (1, 1), 1) + new_im = ImageOps.pad(im, (4, 1)) + assert new_im.load()[2, 0] == 1 + + new_im = ImageOps.pad(im, (1, 4)) + assert new_im.load()[0, 2] == 1 + + def test_pil163(): # Division by zero in equalize if < 255 pixels in image (@PIL163) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 61de3b696f9..ae43fc3bd8e 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -292,10 +292,10 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 else: out = Image.new(image.mode, size, color) if resized.width != size[0]: - x = int((size[0] - resized.width) * max(0, min(centering[0], 1))) + x = round((size[0] - resized.width) * max(0, min(centering[0], 1))) out.paste(resized, (x, 0)) else: - y = int((size[1] - resized.height) * max(0, min(centering[1], 1))) + y = round((size[1] - resized.height) * max(0, min(centering[1], 1))) out.paste(resized, (0, y)) return out From 826ab4b17c1c4622a7210b4c72d50606ed2b2d2a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 18:15:57 -0500 Subject: [PATCH 164/242] remove unused asserts An exception occurs before they would be checked. --- Tests/test_image_filter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index e12e73f9774..1cee8d2c833 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -42,8 +42,6 @@ def test_sanity_error(mode): with pytest.raises(TypeError): im = hopper(mode) out = im.filter("hello") - assert out.mode == im.mode - assert out.size == im.size # crashes on small images From 65694f3fb82bd6b29f1b8750f730ba311e41f8e5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 18:21:27 -0500 Subject: [PATCH 165/242] parametrize test_rankfilter_error() --- Tests/test_image_filter.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 1cee8d2c833..ee645bd47ab 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -96,7 +96,8 @@ def test_rankfilter(mode, expected): assert (minimum, med, maximum) == expected -def test_rankfilter_error(): +@pytest.mark.parametrize("filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)) +def test_rankfilter_error(filter): with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) @@ -104,9 +105,7 @@ def test_rankfilter_error(): # 0 1 2 # 3 4 5 # 6 7 8 - im.filter(ImageFilter.MinFilter).getpixel((1, 1)) - im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) - im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) + im.filter(filter).getpixel((1, 1)) def test_rankfilter_properties(): From 972961c9fec94969b6ef61a6bbd2443e467a3441 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Aug 2022 23:22:06 +0000 Subject: [PATCH 166/242] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_image_filter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index ee645bd47ab..ec215cd7525 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -96,7 +96,9 @@ def test_rankfilter(mode, expected): assert (minimum, med, maximum) == expected -@pytest.mark.parametrize("filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)) +@pytest.mark.parametrize( + "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) +) def test_rankfilter_error(filter): with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) From 2fd3cb55d208237a1fd3812598bb2e1cbc3d4c8a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 24 Aug 2022 19:13:50 -0500 Subject: [PATCH 167/242] remove unused variable --- Tests/test_image_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index ec215cd7525..bec7f21e947 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -41,7 +41,7 @@ def test_sanity(filter_to_apply, mode): def test_sanity_error(mode): with pytest.raises(TypeError): im = hopper(mode) - out = im.filter("hello") + im.filter("hello") # crashes on small images From aa5d67e49281b86631a5b8a7ffc42446dd834265 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 25 Aug 2022 02:57:07 +0200 Subject: [PATCH 168/242] convert TestImageFont and TestImageFont_RaqmLayout into a test fixture --- Tests/test_imagefont.py | 1694 +++++++++++++++++++-------------------- 1 file changed, 832 insertions(+), 862 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 16da87d469a..f8ecc193a7c 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -28,497 +28,527 @@ pytestmark = skip_unless_feature("freetype2") -class TestImageFont: - LAYOUT_ENGINE = ImageFont.Layout.BASIC +def test_sanity(): + assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) + + +@pytest.fixture( + scope="module", + params=[ + ImageFont.Layout.BASIC, + pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), + ], +) +def layout_engine(request): + return request.param + + +@pytest.fixture(scope="module") +def font(layout_engine): + return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) + + +def test_font_properties(font): + assert font.path == FONT_PATH + assert font.size == FONT_SIZE + + font_copy = font.font_variant() + assert font_copy.path == FONT_PATH + assert font_copy.size == FONT_SIZE + + font_copy = font.font_variant(size=FONT_SIZE + 1) + assert font_copy.size == FONT_SIZE + 1 + + second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" + font_copy = font.font_variant(font=second_font_path) + assert font_copy.path == second_font_path - def get_font(self): - return ImageFont.truetype( - FONT_PATH, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE - ) - def test_sanity(self): - assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) +def _render(font, layout_engine): + txt = "Hello World!" + ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) + ttf.getbbox(txt) - def test_font_properties(self): - ttf = self.get_font() - assert ttf.path == FONT_PATH - assert ttf.size == FONT_SIZE + img = Image.new("RGB", (256, 64), "white") + d = ImageDraw.Draw(img) + d.text((10, 10), txt, font=ttf, fill="black") - ttf_copy = ttf.font_variant() - assert ttf_copy.path == FONT_PATH - assert ttf_copy.size == FONT_SIZE + return img - ttf_copy = ttf.font_variant(size=FONT_SIZE + 1) - assert ttf_copy.size == FONT_SIZE + 1 - second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" - ttf_copy = ttf.font_variant(font=second_font_path) - assert ttf_copy.path == second_font_path +def test_font_with_name(layout_engine): + _render(FONT_PATH, layout_engine) - def test_font_with_name(self): - self.get_font() - self._render(FONT_PATH) - def _font_as_bytes(self): +def test_font_with_filelike(layout_engine): + def _font_as_bytes(): with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes - def test_font_with_filelike(self): - ttf = ImageFont.truetype( - self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE - ) - ttf_copy = ttf.font_variant() - assert ttf_copy.font_bytes == ttf.font_bytes + ttf = ImageFont.truetype(_font_as_bytes(), FONT_SIZE, layout_engine=layout_engine) + ttf_copy = ttf.font_variant() + assert ttf_copy.font_bytes == ttf.font_bytes - self._render(self._font_as_bytes()) - # Usage note: making two fonts from the same buffer fails. - # shared_bytes = self._font_as_bytes() - # self._render(shared_bytes) - # with pytest.raises(Exception): - # _render(shared_bytes) + _render(_font_as_bytes(), layout_engine) + # Usage note: making two fonts from the same buffer fails. + # shared_bytes = _font_as_bytes() + # _render(shared_bytes) + # with pytest.raises(Exception): + # _render(shared_bytes) - def test_font_with_open_file(self): - with open(FONT_PATH, "rb") as f: - self._render(f) - def test_non_ascii_path(self, tmp_path): - tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) - try: - shutil.copy(FONT_PATH, tempfile) - except UnicodeEncodeError: - pytest.skip("Non-ASCII path could not be created") +def test_font_with_open_file(layout_engine): + with open(FONT_PATH, "rb") as f: + _render(f, layout_engine) - ImageFont.truetype(tempfile, FONT_SIZE) - def _render(self, font): - txt = "Hello World!" - ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) - ttf.getbbox(txt) +def test_render_equal(layout_engine): + img_path = _render(FONT_PATH, layout_engine) + with open(FONT_PATH, "rb") as f: + font_filelike = BytesIO(f.read()) + img_filelike = _render(font_filelike, layout_engine) - img = Image.new("RGB", (256, 64), "white") - d = ImageDraw.Draw(img) - d.text((10, 10), txt, font=ttf, fill="black") + assert_image_equal(img_path, img_filelike) - return img - def test_render_equal(self): - img_path = self._render(FONT_PATH) - with open(FONT_PATH, "rb") as f: - font_filelike = BytesIO(f.read()) - img_filelike = self._render(font_filelike) +def test_non_ascii_path(tmp_path, layout_engine): + tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) + try: + shutil.copy(FONT_PATH, tempfile) + except UnicodeEncodeError: + pytest.skip("Non-ASCII path could not be created") - assert_image_equal(img_path, img_filelike) + ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) - def test_transparent_background(self): - im = Image.new(mode="RGBA", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() - txt = "Hello World!" - draw.text((10, 10), txt, font=ttf) +def test_transparent_background(font): + im = Image.new(mode="RGBA", size=(300, 100)) + draw = ImageDraw.Draw(im) - target = "Tests/images/transparent_background_text.png" - assert_image_similar_tofile(im, target, 4.09) + txt = "Hello World!" + draw.text((10, 10), txt, font=font) - target = "Tests/images/transparent_background_text_L.png" - assert_image_similar_tofile(im.convert("L"), target, 0.01) + target = "Tests/images/transparent_background_text.png" + assert_image_similar_tofile(im, target, 4.09) - def test_I16(self): - im = Image.new(mode="I;16", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) - txt = "Hello World!" - draw.text((10, 10), txt, font=ttf) - target = "Tests/images/transparent_background_text_L.png" - assert_image_similar_tofile(im.convert("L"), target, 0.01) +def test_I16(font): + im = Image.new(mode="I;16", size=(300, 100)) + draw = ImageDraw.Draw(im) - def test_textbbox_equal(self): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() + txt = "Hello World!" + draw.text((10, 10), txt, font=font) - txt = "Hello World!" - bbox = draw.textbbox((10, 10), txt, ttf) - draw.text((10, 10), txt, font=ttf) - draw.rectangle(bbox) + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) - assert_image_similar_tofile( - im, "Tests/images/rectangle_surrounding_text.png", 2.5 - ) - @pytest.mark.parametrize( - "text, mode, font, size, length_basic, length_raqm", - ( - # basic test - ("text", "L", "FreeMono.ttf", 15, 36, 36), - ("text", "1", "FreeMono.ttf", 15, 36, 36), - # issue 4177 - ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875), - ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875), - # test 'l' not including extra margin - # using exact value 2047 / 64 for raqm, checked with debugger - ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), - ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), - ), - ) - def test_getlength(self, text, mode, font, size, length_basic, length_raqm): - f = ImageFont.truetype( - "Tests/fonts/" + font, size, layout_engine=self.LAYOUT_ENGINE - ) +def test_textbbox_equal(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) - im = Image.new(mode, (1, 1), 0) - d = ImageDraw.Draw(im) + txt = "Hello World!" + bbox = draw.textbbox((10, 10), txt, font) + draw.text((10, 10), txt, font=font) + draw.rectangle(bbox) - if self.LAYOUT_ENGINE == ImageFont.Layout.BASIC: - length = d.textlength(text, f) - assert length == length_basic - else: - # disable kerning, kerning metrics changed - length = d.textlength(text, f, features=["-kern"]) - assert length == length_raqm - - def test_render_multiline(self): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() - line_spacing = ttf.getbbox("A")[3] + 4 - lines = TEST_TEXT.split("\n") - y = 0 - for line in lines: - draw.text((0, y), line, font=ttf) - y += line_spacing - - # some versions of freetype have different horizontal spacing. - # setting a tight epsilon, I'm showing the original test failure - # at epsilon = ~38. - assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) - - def test_render_multiline_text(self): - ttf = self.get_font() - - # Test that text() correctly connects to multiline_text() - # and that align defaults to left - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), TEST_TEXT, font=ttf) - - assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01) - - # Test that text() can pass on additional arguments - # to multiline_text() - draw.text( - (0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, spacing=4, align="left" - ) - draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left") + assert_image_similar_tofile(im, "Tests/images/rectangle_surrounding_text.png", 2.5) - # Test align center and right - for align, ext in {"center": "_center", "right": "_right"}.items(): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) - assert_image_similar_tofile( - im, "Tests/images/multiline_text" + ext + ".png", 0.01 - ) +@pytest.mark.parametrize( + "text, mode, fontname, size, length_basic, length_raqm", + ( + # basic test + ("text", "L", "FreeMono.ttf", 15, 36, 36), + ("text", "1", "FreeMono.ttf", 15, 36, 36), + # issue 4177 + ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875), + ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875), + # test 'l' not including extra margin + # using exact value 2047 / 64 for raqm, checked with debugger + ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), + ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), + ), +) +def test_getlength( + text, mode, fontname, size, layout_engine, length_basic, length_raqm +): + f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) + + im = Image.new(mode, (1, 1), 0) + d = ImageDraw.Draw(im) + + if layout_engine == ImageFont.Layout.BASIC: + length = d.textlength(text, f) + assert length == length_basic + else: + # disable kerning, kerning metrics changed + length = d.textlength(text, f, features=["-kern"]) + assert length == length_raqm - def test_unknown_align(self): - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = self.get_font() - # Act/Assert - with pytest.raises(ValueError): - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align="unknown") +def test_render_multiline(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + line_spacing = font.getbbox("A")[3] + 4 + lines = TEST_TEXT.split("\n") + y = 0 + for line in lines: + draw.text((0, y), line, font=font) + y += line_spacing + + # some versions of freetype have different horizontal spacing. + # setting a tight epsilon, I'm showing the original test failure + # at epsilon = ~38. + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) + + +def test_render_multiline_text(font): + # Test that text() correctly connects to multiline_text() + # and that align defaults to left + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), TEST_TEXT, font=font) + + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01) - def test_draw_align(self): - im = Image.new("RGB", (300, 100), "white") - draw = ImageDraw.Draw(im) - ttf = self.get_font() - line = "some text" - draw.text((100, 40), line, (0, 0, 0), font=ttf, align="left") + # Test that text() can pass on additional arguments + # to multiline_text() + draw.text( + (0, 0), TEST_TEXT, fill=None, font=font, anchor=None, spacing=4, align="left" + ) + draw.text((0, 0), TEST_TEXT, None, font, None, 4, "left") - def test_multiline_size(self): - ttf = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - with pytest.warns(DeprecationWarning) as log: - # Test that textsize() correctly connects to multiline_textsize() - assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize( - TEST_TEXT, font=ttf - ) +@pytest.mark.parametrize( + "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) +) +def test_render_multiline_text_align(font, align, ext): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) + + assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) + + +def test_unknown_align(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Act/Assert + with pytest.raises(ValueError): + draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") + - # Test that multiline_textsize corresponds to ImageFont.textsize() - # for single line text - assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf) +def test_draw_align(font): + im = Image.new("RGB", (300, 100), "white") + draw = ImageDraw.Draw(im) + line = "some text" + draw.text((100, 40), line, (0, 0, 0), font=font, align="left") - # Test that textsize() can pass on additional arguments - # to multiline_textsize() - draw.textsize(TEST_TEXT, font=ttf, spacing=4) - draw.textsize(TEST_TEXT, ttf, 4) - assert len(log) == 6 - def test_multiline_bbox(self): - ttf = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) +def test_multiline_size(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) - # Test that textbbox() correctly connects to multiline_textbbox() - assert draw.textbbox((0, 0), TEST_TEXT, font=ttf) == draw.multiline_textbbox( - (0, 0), TEST_TEXT, font=ttf + with pytest.warns(DeprecationWarning) as log: + # Test that textsize() correctly connects to multiline_textsize() + assert draw.textsize(TEST_TEXT, font=font) == draw.multiline_textsize( + TEST_TEXT, font=font ) - # Test that multiline_textbbox corresponds to ImageFont.textbbox() + # Test that multiline_textsize corresponds to ImageFont.textsize() # for single line text - assert ttf.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=ttf) + assert font.getsize("A") == draw.multiline_textsize("A", font=font) + + # Test that textsize() can pass on additional arguments + # to multiline_textsize() + draw.textsize(TEST_TEXT, font=font, spacing=4) + draw.textsize(TEST_TEXT, font, 4) + assert len(log) == 6 - # Test that textbbox() can pass on additional arguments - # to multiline_textbbox() - draw.textbbox((0, 0), TEST_TEXT, font=ttf, spacing=4) - def test_multiline_width(self): - ttf = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) +def test_multiline_bbox(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Test that textbbox() correctly connects to multiline_textbbox() + assert draw.textbbox((0, 0), TEST_TEXT, font=font) == draw.multiline_textbbox( + (0, 0), TEST_TEXT, font=font + ) + + # Test that multiline_textbbox corresponds to ImageFont.textbbox() + # for single line text + assert font.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=font) + + # Test that textbbox() can pass on additional arguments + # to multiline_textbbox() + draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) + +def test_multiline_width(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + assert ( + draw.textbbox((0, 0), "longest line", font=font)[2] + == draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2] + ) + with pytest.warns(DeprecationWarning) as log: assert ( - draw.textbbox((0, 0), "longest line", font=ttf)[2] - == draw.multiline_textbbox((0, 0), "longest line\nline", font=ttf)[2] + draw.textsize("longest line", font=font)[0] + == draw.multiline_textsize("longest line\nline", font=font)[0] ) - with pytest.warns(DeprecationWarning) as log: - assert ( - draw.textsize("longest line", font=ttf)[0] - == draw.multiline_textsize("longest line\nline", font=ttf)[0] - ) - assert len(log) == 2 + assert len(log) == 2 - def test_multiline_spacing(self): - ttf = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) +def test_multiline_spacing(font): + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) - assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5) + assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5) - def test_rotated_transposed_font(self): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = self.get_font() - orientation = Image.Transpose.ROTATE_90 - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) +@pytest.mark.parametrize( + "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) +) +def test_rotated_transposed_font(font, orientation): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" + + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) + + # Original font + draw.font = font + with pytest.warns(DeprecationWarning) as log: + box_size_a = draw.textsize(word) + assert box_size_a == font.getsize(word) + assert len(log) == 2 + bbox_a = draw.textbbox((10, 10), word) + + # Rotated font + draw.font = transposed_font + with pytest.warns(DeprecationWarning) as log: + box_size_b = draw.textsize(word) + assert box_size_b == transposed_font.getsize(word) + assert len(log) == 2 + bbox_b = draw.textbbox((20, 20), word) + + # Check (w,h) of box a is (h,w) of box b + assert box_size_a[0] == box_size_b[1] + assert box_size_a[1] == box_size_b[0] + + # Check bbox b is (20, 20, 20 + h, 20 + w) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] + assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + + # text length is undefined for vertical text + pytest.raises(ValueError, draw.textlength, word) - # Original font - draw.font = font - with pytest.warns(DeprecationWarning) as log: - box_size_a = draw.textsize(word) - assert box_size_a == font.getsize(word) - assert len(log) == 2 - bbox_a = draw.textbbox((10, 10), word) - # Rotated font - draw.font = transposed_font - with pytest.warns(DeprecationWarning) as log: - box_size_b = draw.textsize(word) - assert box_size_b == transposed_font.getsize(word) - assert len(log) == 2 - bbox_b = draw.textbbox((20, 20), word) +@pytest.mark.parametrize( + "orientation", + ( + None, + Image.Transpose.ROTATE_180, + Image.Transpose.FLIP_LEFT_RIGHT, + Image.Transpose.FLIP_TOP_BOTTOM, + ), +) +def test_unrotated_transposed_font(font, orientation): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" - # Check (w,h) of box a is (h,w) of box b - assert box_size_a[0] == box_size_b[1] - assert box_size_a[1] == box_size_b[0] + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Check bbox b is (20, 20, 20 + h, 20 + w) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] - assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + # Original font + draw.font = font + with pytest.warns(DeprecationWarning) as log: + box_size_a = draw.textsize(word) + assert len(log) == 1 + bbox_a = draw.textbbox((10, 10), word) + length_a = draw.textlength(word) - # text length is undefined for vertical text - pytest.raises(ValueError, draw.textlength, word) + # Rotated font + draw.font = transposed_font + with pytest.warns(DeprecationWarning) as log: + box_size_b = draw.textsize(word) + assert len(log) == 1 + bbox_b = draw.textbbox((20, 20), word) + length_b = draw.textlength(word) - def test_unrotated_transposed_font(self): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = self.get_font() + # Check boxes a and b are same size + assert box_size_a == box_size_b - orientation = None - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) + # Check bbox b is (20, 20, 20 + w, 20 + h) + assert bbox_b[0] == 20 + assert bbox_b[1] == 20 + assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] + assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] - # Original font - draw.font = font - with pytest.warns(DeprecationWarning) as log: - box_size_a = draw.textsize(word) - assert len(log) == 1 - bbox_a = draw.textbbox((10, 10), word) - length_a = draw.textlength(word) + assert length_a == length_b - # Rotated font - draw.font = transposed_font - with pytest.warns(DeprecationWarning) as log: - box_size_b = draw.textsize(word) - assert len(log) == 1 - bbox_b = draw.textbbox((20, 20), word) - length_b = draw.textlength(word) - # Check boxes a and b are same size - assert box_size_a == box_size_b +@pytest.mark.parametrize( + "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) +) +def test_rotated_transposed_font_get_mask(font, orientation): + # Arrange + text = "mask this" + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) + + # Act + mask = transposed_font.getmask(text) - # Check bbox b is (20, 20, 20 + w, 20 + h) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] - assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] + # Assert + assert mask.size == (13, 108) - assert length_a == length_b - def test_rotated_transposed_font_get_mask(self): - # Arrange - text = "mask this" - font = self.get_font() - orientation = Image.Transpose.ROTATE_90 - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) +@pytest.mark.parametrize( + "orientation", + ( + None, + Image.Transpose.ROTATE_180, + Image.Transpose.FLIP_LEFT_RIGHT, + Image.Transpose.FLIP_TOP_BOTTOM, + ), +) +def test_unrotated_transposed_font_get_mask(font, orientation): + # Arrange + text = "mask this" + transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Act - mask = transposed_font.getmask(text) + # Act + mask = transposed_font.getmask(text) - # Assert - assert mask.size == (13, 108) + # Assert + assert mask.size == (108, 13) - def test_unrotated_transposed_font_get_mask(self): - # Arrange - text = "mask this" - font = self.get_font() - orientation = None - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - # Act - mask = transposed_font.getmask(text) +def test_free_type_font_get_name(font): + assert ("FreeMono", "Regular") == font.getname() - # Assert - assert mask.size == (108, 13) - def test_free_type_font_get_name(self): - # Arrange - font = self.get_font() +def test_free_type_font_get_metrics(font): + ascent, descent = font.getmetrics() - # Act - name = font.getname() + assert isinstance(ascent, int) + assert isinstance(descent, int) + assert (ascent, descent) == (16, 4) - # Assert - assert ("FreeMono", "Regular") == name - def test_free_type_font_get_metrics(self): - # Arrange - font = self.get_font() +def test_free_type_font_get_offset(font): + # Arrange + text = "offset this" - # Act - ascent, descent = font.getmetrics() + # Act + with pytest.warns(DeprecationWarning) as log: + offset = font.getoffset(text) - # Assert - assert isinstance(ascent, int) - assert isinstance(descent, int) - assert (ascent, descent) == (16, 4) # too exact check? + # Assert + assert len(log) == 1 + assert offset == (0, 3) - def test_free_type_font_get_offset(self): - # Arrange - font = self.get_font() - text = "offset this" - # Act - with pytest.warns(DeprecationWarning) as log: - offset = font.getoffset(text) +def test_free_type_font_get_mask(font): + # Arrange + text = "mask this" - # Assert - assert len(log) == 1 - assert offset == (0, 3) + # Act + mask = font.getmask(text) - def test_free_type_font_get_mask(self): - # Arrange - font = self.get_font() - text = "mask this" + # Assert + assert mask.size == (108, 13) - # Act - mask = font.getmask(text) - # Assert - assert mask.size == (108, 13) +def test_load_path_not_found(): + # Arrange + filename = "somefilenamethatdoesntexist.ttf" - def test_load_path_not_found(self): - # Arrange - filename = "somefilenamethatdoesntexist.ttf" + # Act/Assert + with pytest.raises(OSError): + ImageFont.load_path(filename) + with pytest.raises(OSError): + ImageFont.truetype(filename) - # Act/Assert - with pytest.raises(OSError): - ImageFont.load_path(filename) + +def test_load_non_font_bytes(): + with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): - ImageFont.truetype(filename) - - def test_load_non_font_bytes(self): - with open("Tests/images/hopper.jpg", "rb") as f: - with pytest.raises(OSError): - ImageFont.truetype(f) - - def test_default_font(self): - # Arrange - txt = 'This is a "better than nothing" default font.' - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - - # Act - default_font = ImageFont.load_default() - draw.text((10, 10), txt, font=default_font) - - # Assert - assert_image_equal_tofile(im, "Tests/images/default_font.png") - - def test_getbbox_empty(self): - # issue #2614 - font = self.get_font() - # should not crash. - assert (0, 0, 0, 0) == font.getbbox("") - - def test_render_empty(self): - # issue 2666 - font = self.get_font() - im = Image.new(mode="RGB", size=(300, 100)) - target = im.copy() - draw = ImageDraw.Draw(im) - # should not crash here. - draw.text((10, 10), "", font=font) - assert_image_equal(im, target) - - def test_unicode_pilfont(self): - # should not segfault, should return UnicodeDecodeError - # issue #2826 - font = ImageFont.load_default() - with pytest.raises(UnicodeEncodeError): - font.getbbox("’") - - def test_unicode_extended(self): - # issue #3777 - text = "A\u278A\U0001F12B" - target = "Tests/images/unicode_extended.png" - - ttf = ImageFont.truetype( - "Tests/fonts/NotoSansSymbols-Regular.ttf", - FONT_SIZE, - layout_engine=self.LAYOUT_ENGINE, - ) - img = Image.new("RGB", (100, 60)) - d = ImageDraw.Draw(img) - d.text((10, 10), text, font=ttf) + ImageFont.truetype(f) + + +def test_default_font(): + # Arrange + txt = 'This is a "better than nothing" default font.' + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) + + # Assert + assert_image_equal_tofile(im, "Tests/images/default_font.png") + + +def test_getbbox_empty(font): + # issue #2614, should not crash. + assert (0, 0, 0, 0) == font.getbbox("") + + +def test_render_empty(font): + # issue 2666 + im = Image.new(mode="RGB", size=(300, 100)) + target = im.copy() + draw = ImageDraw.Draw(im) + # should not crash here. + draw.text((10, 10), "", font=font) + assert_image_equal(im, target) + + +def test_unicode_pilfont(): + # should not segfault, should return UnicodeDecodeError + # issue #2826 + font = ImageFont.load_default() + with pytest.raises(UnicodeEncodeError): + font.getbbox("’") + + +def test_unicode_extended(layout_engine): + # issue #3777 + text = "A\u278A\U0001F12B" + target = "Tests/images/unicode_extended.png" + + ttf = ImageFont.truetype( + "Tests/fonts/NotoSansSymbols-Regular.ttf", + FONT_SIZE, + layout_engine=layout_engine, + ) + img = Image.new("RGB", (100, 60)) + d = ImageDraw.Draw(img) + d.text((10, 10), text, font=ttf) + + # fails with 14.7 + assert_image_similar_tofile(img, target, 6.2) - # fails with 14.7 - assert_image_similar_tofile(img, target, 6.2) - def _test_fake_loading_font(self, monkeypatch, path_to_fake, fontname): +@pytest.mark.parametrize( + "platform, font_directory", + (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), +) +@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") +def test_find_font(monkeypatch, platform, font_directory): + def _test_fake_loading_font(path_to_fake, fontname): # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: @@ -539,543 +569,483 @@ def loadable_font(filepath, size, index, encoding, *args, **kwargs): name = font.getname() assert ("FreeMono", "Regular") == name - @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") - def test_find_linux_font(self, monkeypatch): - # A lot of mocking here - this is more for hitting code and - # catching syntax like errors - font_directory = "/usr/local/share/fonts" - monkeypatch.setattr(sys, "platform", "linux") + # A lot of mocking here - this is more for hitting code and + # catching syntax like errors + monkeypatch.setattr(sys, "platform", platform) + if platform == "linux": monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - def fake_walker(path): - if path == font_directory: - return [ - ( - path, - [], - ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], - ) - ] - return [(path, [], ["some_random_font.ttf"])] - - monkeypatch.setattr(os, "walk", fake_walker) - # Test that the font loads both with and without the - # extension - self._test_fake_loading_font( - monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf" - ) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Arial.ttf", "Arial" - ) - - # Test that non-ttf fonts can be found without the - # extension - self._test_fake_loading_font( - monkeypatch, font_directory + "/Single.otf", "Single" - ) - - # Test that ttf fonts are preferred if the extension is - # not specified - self._test_fake_loading_font( - monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate" + def fake_walker(path): + if path == font_directory: + return [ + ( + path, + [], + ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], + ) + ] + return [(path, [], ["some_random_font.ttf"])] + + monkeypatch.setattr(os, "walk", fake_walker) + + # Test that the font loads both with and without the extension + _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf") + _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") + + # Test that non-ttf fonts can be found without the extension + _test_fake_loading_font(font_directory + "/Single.otf", "Single") + + # Test that ttf fonts are preferred if the extension is not specified + _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") + + +def test_imagefont_getters(font): + assert font.getmetrics() == (16, 4) + assert font.font.ascent == 16 + assert font.font.descent == 4 + assert font.font.height == 20 + assert font.font.x_ppem == 20 + assert font.font.y_ppem == 20 + assert font.font.glyphs == 4177 + assert font.getbbox("A") == (0, 4, 12, 16) + assert font.getbbox("AB") == (0, 4, 24, 16) + assert font.getbbox("M") == (0, 4, 12, 16) + assert font.getbbox("y") == (0, 7, 12, 20) + assert font.getbbox("a") == (0, 7, 12, 16) + assert font.getlength("A") == 12 + assert font.getlength("AB") == 24 + assert font.getlength("M") == 12 + assert font.getlength("y") == 12 + assert font.getlength("a") == 12 + with pytest.warns(DeprecationWarning) as log: + assert font.getsize("A") == (12, 16) + assert font.getsize("AB") == (24, 16) + assert font.getsize("M") == (12, 16) + assert font.getsize("y") == (12, 20) + assert font.getsize("a") == (12, 16) + assert font.getsize_multiline("A") == (12, 16) + assert font.getsize_multiline("AB") == (24, 16) + assert font.getsize_multiline("a") == (12, 16) + assert font.getsize_multiline("ABC\n") == (36, 36) + assert font.getsize_multiline("ABC\nA") == (36, 36) + assert font.getsize_multiline("ABC\nAaaa") == (48, 36) + assert len(log) == 11 + + +def test_getsize_stroke(font): + for stroke_width in [0, 2]: + assert font.getbbox("A", stroke_width=stroke_width) == ( + 0 - stroke_width, + 4 - stroke_width, + 12 + stroke_width, + 16 + stroke_width, ) + with pytest.warns(DeprecationWarning) as log: + assert font.getsize("A", stroke_width=stroke_width) == ( + 12 + stroke_width * 2, + 16 + stroke_width * 2, + ) + assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( + 48 + stroke_width * 2, + 36 + stroke_width * 4, + ) + assert len(log) == 2 - @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") - def test_find_macos_font(self, monkeypatch): - # Like the linux test, more cover hitting code rather than testing - # correctness. - font_directory = "/System/Library/Fonts" - monkeypatch.setattr(sys, "platform", "darwin") - - def fake_walker(path): - if path == font_directory: - return [ - ( - path, - [], - ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"], - ) - ] - return [(path, [], ["some_random_font.ttf"])] - monkeypatch.setattr(os, "walk", fake_walker) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf" - ) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Arial.ttf", "Arial" - ) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Single.otf", "Single" - ) - self._test_fake_loading_font( - monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate" - ) +def test_complex_font_settings(): + t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC) + with pytest.raises(KeyError): + t.getmask("абвг", direction="rtl") + with pytest.raises(KeyError): + t.getmask("абвг", features=["-kern"]) + with pytest.raises(KeyError): + t.getmask("абвг", language="sr") - def test_imagefont_getters(self): - # Arrange - t = self.get_font() - - # Act / Assert - assert t.getmetrics() == (16, 4) - assert t.font.ascent == 16 - assert t.font.descent == 4 - assert t.font.height == 20 - assert t.font.x_ppem == 20 - assert t.font.y_ppem == 20 - assert t.font.glyphs == 4177 - assert t.getbbox("A") == (0, 4, 12, 16) - assert t.getbbox("AB") == (0, 4, 24, 16) - assert t.getbbox("M") == (0, 4, 12, 16) - assert t.getbbox("y") == (0, 7, 12, 20) - assert t.getbbox("a") == (0, 7, 12, 16) - assert t.getlength("A") == 12 - assert t.getlength("AB") == 24 - assert t.getlength("M") == 12 - assert t.getlength("y") == 12 - assert t.getlength("a") == 12 - with pytest.warns(DeprecationWarning) as log: - assert t.getsize("A") == (12, 16) - assert t.getsize("AB") == (24, 16) - assert t.getsize("M") == (12, 16) - assert t.getsize("y") == (12, 20) - assert t.getsize("a") == (12, 16) - assert t.getsize_multiline("A") == (12, 16) - assert t.getsize_multiline("AB") == (24, 16) - assert t.getsize_multiline("a") == (12, 16) - assert t.getsize_multiline("ABC\n") == (36, 36) - assert t.getsize_multiline("ABC\nA") == (36, 36) - assert t.getsize_multiline("ABC\nAaaa") == (48, 36) - assert len(log) == 11 - - def test_getsize_stroke(self): - # Arrange - t = self.get_font() - - # Act / Assert - for stroke_width in [0, 2]: - assert t.getbbox("A", stroke_width=stroke_width) == ( - 0 - stroke_width, - 4 - stroke_width, - 12 + stroke_width, - 16 + stroke_width, - ) - with pytest.warns(DeprecationWarning) as log: - assert t.getsize("A", stroke_width=stroke_width) == ( - 12 + stroke_width * 2, - 16 + stroke_width * 2, - ) - assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( - 48 + stroke_width * 2, - 36 + stroke_width * 4, - ) - assert len(log) == 2 - - def test_complex_font_settings(self): - # Arrange - t = self.get_font() - # Act / Assert - if t.layout_engine == ImageFont.Layout.BASIC: - with pytest.raises(KeyError): - t.getmask("абвг", direction="rtl") - with pytest.raises(KeyError): - t.getmask("абвг", features=["-kern"]) - with pytest.raises(KeyError): - t.getmask("абвг", language="sr") - - def test_variation_get(self): - font = self.get_font() - - freetype = parse_version(features.version_module("freetype2")) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.get_variation_names() - with pytest.raises(NotImplementedError): - font.get_variation_axes() - return - with pytest.raises(OSError): +def test_variation_get(font): + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): font.get_variation_names() - with pytest.raises(OSError): + with pytest.raises(NotImplementedError): font.get_variation_axes() - - font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") - assert font.get_variation_names(), [ - b"ExtraLight", - b"Light", - b"Regular", - b"Semibold", - b"Bold", - b"Black", - b"Black Medium Contrast", - b"Black High Contrast", - b"Default", - ] - assert font.get_variation_axes() == [ - {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, - {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, - ] - - font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf") - assert font.get_variation_names() == [ - b"20", - b"40", - b"60", - b"80", - b"100", - b"120", - b"140", - b"160", - b"180", - b"200", - b"220", - b"240", - b"260", - b"280", - b"300", - b"Regular", - ] - assert font.get_variation_axes() == [ - {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0} - ] - - def _check_text(self, font, path, epsilon): - im = Image.new("RGB", (100, 75), "white") - d = ImageDraw.Draw(im) - d.text((10, 10), "Text", font=font, fill="black") - - try: + return + + with pytest.raises(OSError): + font.get_variation_names() + with pytest.raises(OSError): + font.get_variation_axes() + + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf") + assert font.get_variation_names(), [ + b"ExtraLight", + b"Light", + b"Regular", + b"Semibold", + b"Bold", + b"Black", + b"Black Medium Contrast", + b"Black High Contrast", + b"Default", + ] + assert font.get_variation_axes() == [ + {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389}, + {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0}, + ] + + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf") + assert font.get_variation_names() == [ + b"20", + b"40", + b"60", + b"80", + b"100", + b"120", + b"140", + b"160", + b"180", + b"200", + b"220", + b"240", + b"260", + b"280", + b"300", + b"Regular", + ] + assert font.get_variation_axes() == [ + {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0} + ] + + +def _check_text(font, path, epsilon): + im = Image.new("RGB", (100, 75), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), "Text", font=font, fill="black") + + try: + assert_image_similar_tofile(im, path, epsilon) + except AssertionError: + if "_adobe" in path: + path = path.replace("_adobe", "_adobe_older_harfbuzz") assert_image_similar_tofile(im, path, epsilon) - except AssertionError: - if "_adobe" in path: - path = path.replace("_adobe", "_adobe_older_harfbuzz") - assert_image_similar_tofile(im, path, epsilon) - else: - raise - - def test_variation_set_by_name(self): - font = self.get_font() - - freetype = parse_version(features.version_module("freetype2")) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_name("Bold") - return + else: + raise - with pytest.raises(OSError): + +def test_variation_set_by_name(font): + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): font.set_variation_by_name("Bold") + return - font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) - self._check_text(font, "Tests/images/variation_adobe.png", 11) - for name in ["Bold", b"Bold"]: - font.set_variation_by_name(name) - self._check_text(font, "Tests/images/variation_adobe_name.png", 11) + with pytest.raises(OSError): + font.set_variation_by_name("Bold") - font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) - self._check_text(font, "Tests/images/variation_tiny.png", 40) - for name in ["200", b"200"]: - font.set_variation_by_name(name) - self._check_text(font, "Tests/images/variation_tiny_name.png", 40) + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + _check_text(font, "Tests/images/variation_adobe.png", 11) + for name in ["Bold", b"Bold"]: + font.set_variation_by_name(name) + _check_text(font, "Tests/images/variation_adobe_name.png", 11) - def test_variation_set_by_axes(self): - font = self.get_font() + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) + _check_text(font, "Tests/images/variation_tiny.png", 40) + for name in ["200", b"200"]: + font.set_variation_by_name(name) + _check_text(font, "Tests/images/variation_tiny_name.png", 40) - freetype = parse_version(features.version_module("freetype2")) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_axes([100]) - return - with pytest.raises(OSError): - font.set_variation_by_axes([500, 50]) +def test_variation_set_by_axes(font): + freetype = parse_version(features.version_module("freetype2")) + if freetype < parse_version("2.9.1"): + with pytest.raises(NotImplementedError): + font.set_variation_by_axes([100]) + return - font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + with pytest.raises(OSError): font.set_variation_by_axes([500, 50]) - self._check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) - font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) - font.set_variation_by_axes([100]) - self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) + font.set_variation_by_axes([500, 50]) + _check_text(font, "Tests/images/variation_adobe_axes.png", 11.05) - def test_textbbox_non_freetypefont(self): - im = Image.new("RGB", (200, 200)) - d = ImageDraw.Draw(im) - default_font = ImageFont.load_default() - with pytest.warns(DeprecationWarning) as log: - width, height = d.textsize("test", font=default_font) - assert len(log) == 1 - assert d.textlength("test", font=default_font) == width - assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) - - @pytest.mark.parametrize( - "anchor, left, top", - ( - # test horizontal anchors - ("ls", 0, -36), - ("ms", -64, -36), - ("rs", -128, -36), - # test vertical anchors - ("ma", -64, 16), - ("mt", -64, 0), - ("mm", -64, -17), - ("mb", -64, -44), - ("md", -64, -51), - ), - ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), + font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) + font.set_variation_by_axes([100]) + _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + + +def test_textbbox_non_freetypefont(): + im = Image.new("RGB", (200, 200)) + d = ImageDraw.Draw(im) + default_font = ImageFont.load_default() + with pytest.warns(DeprecationWarning) as log: + width, height = d.textsize("test", font=default_font) + assert len(log) == 1 + assert d.textlength("test", font=default_font) == width + assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) + + +@pytest.mark.parametrize( + "anchor, left, top", + ( + # test horizontal anchors + ("ls", 0, -36), + ("ms", -64, -36), + ("rs", -128, -36), + # test vertical anchors + ("ma", -64, 16), + ("mt", -64, 0), + ("mm", -64, -17), + ("mb", -64, -44), + ("md", -64, -51), + ), + ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), +) +def test_anchor(layout_engine, anchor, left, top): + name, text = "quick", "Quick" + path = f"Tests/images/test_anchor_{name}_{anchor}.png" + + if layout_engine == ImageFont.Layout.RAQM: + width, height = (129, 44) + else: + width, height = (128, 44) + + bbox_expected = (left, top, left + width, top + height) + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine ) - def test_anchor(self, anchor, left, top): - name, text = "quick", "Quick" - path = f"Tests/images/test_anchor_{name}_{anchor}.png" - if self.LAYOUT_ENGINE == ImageFont.Layout.RAQM: - width, height = (129, 44) - else: - width, height = (128, 44) + im = Image.new("RGB", (200, 200), "white") + d = ImageDraw.Draw(im) + d.line(((0, 100), (200, 100)), "gray") + d.line(((100, 0), (100, 200)), "gray") + d.text((100, 100), text, fill="black", anchor=anchor, font=f) - bbox_expected = (left, top, left + width, top + height) + assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected - f = ImageFont.truetype( - "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE - ) + assert_image_similar_tofile(im, path, 7) - im = Image.new("RGB", (200, 200), "white") - d = ImageDraw.Draw(im) - d.line(((0, 100), (200, 100)), "gray") - d.line(((100, 0), (100, 200)), "gray") - d.text((100, 100), text, fill="black", anchor=anchor, font=f) - - assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected - - assert_image_similar_tofile(im, path, 7) - - @pytest.mark.parametrize( - "anchor, align", - ( - # test horizontal anchors - ("lm", "left"), - ("lm", "center"), - ("lm", "right"), - ("mm", "left"), - ("mm", "center"), - ("mm", "right"), - ("rm", "left"), - ("rm", "center"), - ("rm", "right"), - # test vertical anchors - ("ma", "center"), - # ("mm", "center"), # duplicate - ("md", "center"), - ), + +@pytest.mark.parametrize( + "anchor, align", + ( + # test horizontal anchors + ("lm", "left"), + ("lm", "center"), + ("lm", "right"), + ("mm", "left"), + ("mm", "center"), + ("mm", "right"), + ("rm", "left"), + ("rm", "center"), + ("rm", "right"), + # test vertical anchors + ("ma", "center"), + # ("mm", "center"), # duplicate + ("md", "center"), + ), +) +def test_anchor_multiline(layout_engine, anchor, align): + target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" + text = "a\nlong\ntext sample" + + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine ) - def test_anchor_multiline(self, anchor, align): - target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" - text = "a\nlong\ntext sample" - f = ImageFont.truetype( - "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE - ) + # test render + im = Image.new("RGB", (600, 400), "white") + d = ImageDraw.Draw(im) + d.line(((0, 200), (600, 200)), "gray") + d.line(((300, 0), (300, 400)), "gray") + d.multiline_text((300, 200), text, fill="black", anchor=anchor, font=f, align=align) - # test render - im = Image.new("RGB", (600, 400), "white") - d = ImageDraw.Draw(im) - d.line(((0, 200), (600, 200)), "gray") - d.line(((300, 0), (300, 400)), "gray") - d.multiline_text( - (300, 200), text, fill="black", anchor=anchor, font=f, align=align - ) + assert_image_similar_tofile(im, target, 4) - assert_image_similar_tofile(im, target, 4) - def test_anchor_invalid(self): - font = self.get_font() - im = Image.new("RGB", (100, 100), "white") - d = ImageDraw.Draw(im) - d.font = font - - for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: - pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) - pytest.raises( - ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor) - ) - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) - for anchor in ["lt", "lb"]: - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) +def test_anchor_invalid(font): + im = Image.new("RGB", (100, 100), "white") + d = ImageDraw.Draw(im) + d.font = font - @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/DejaVuSans-24-{bpp}-stripped.ttf", - 24, - layout_engine=self.LAYOUT_ENGINE, + for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: + pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) + pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) + pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)) + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), + ) + for anchor in ["lt", "lb"]: + pytest.raises( + ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + ) + pytest.raises( + ValueError, + lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), ) - im = Image.new("RGB", (160, 35), "white") - draw = ImageDraw.Draw(im) - draw.text((2, 2), text, "black", font) - assert_image_equal_tofile(im, target) +@pytest.mark.parametrize("bpp", (1, 2, 4, 8)) +def test_bitmap_font(layout_engine, bpp): + text = "Bitmap Font" + layout_name = ["basic", "raqm"][layout_engine] + target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" + font = ImageFont.truetype( + f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf", + 24, + layout_engine=layout_engine, + ) - def test_bitmap_font_stroke(self): - text = "Bitmap Font" - layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE] - target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" - font = ImageFont.truetype( - "Tests/fonts/DejaVuSans/DejaVuSans-24-8-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) - im = Image.new("RGB", (160, 35), "white") - draw = ImageDraw.Draw(im) - draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red") + assert_image_equal_tofile(im, target) - assert_image_similar_tofile(im, target, 0.03) - def test_standard_embedded_color(self): - txt = "Hello World!" - ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE) - ttf.getbbox(txt) +def test_bitmap_font_stroke(layout_engine): + text = "Bitmap Font" + layout_name = ["basic", "raqm"][layout_engine] + target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" + font = ImageFont.truetype( + "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf", + 24, + layout_engine=layout_engine, + ) - im = Image.new("RGB", (300, 64), "white") - d = ImageDraw.Draw(im) - d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red") - assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2) + assert_image_similar_tofile(im, target, 0.03) - 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) +def test_standard_embedded_color(layout_engine): + txt = "Hello World!" + ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) + ttf.getbbox(txt) - d.text((10, 10), "\U0001f469", font=font, embedded_color=True) + im = Image.new("RGB", (300, 64), "white") + d = ImageDraw.Draw(im) + d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) - assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or CBDT support") + assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2) - 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) +def test_cbdt(layout_engine): + try: + font = ImageFont.truetype( + "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine + ) - d.text((10, 10), "\U0001f469", "black", font=font) + im = Image.new("RGB", (150, 150), "white") + d = ImageDraw.Draw(im) - assert_image_similar_tofile( - im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 - ) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or CBDT support") - - def test_sbix(self): - try: - font = ImageFont.truetype( - "Tests/fonts/chromacheck-sbix.woff", - size=300, - layout_engine=self.LAYOUT_ENGINE, - ) + d.text((10, 10), "\U0001f469", font=font, embedded_color=True) - im = Image.new("RGB", (400, 400), "white") - d = ImageDraw.Draw(im) + assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or CBDT support") - d.text((50, 50), "\uE901", font=font, embedded_color=True) - assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or SBIX support") +def test_cbdt_mask(layout_engine): + try: + font = ImageFont.truetype( + "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine + ) - def test_sbix_mask(self): - try: - font = ImageFont.truetype( - "Tests/fonts/chromacheck-sbix.woff", - size=300, - layout_engine=self.LAYOUT_ENGINE, - ) + im = Image.new("RGB", (150, 150), "white") + d = ImageDraw.Draw(im) - im = Image.new("RGB", (400, 400), "white") - d = ImageDraw.Draw(im) + d.text((10, 10), "\U0001f469", "black", font=font) - d.text((50, 50), "\uE901", (100, 0, 0), font=font) + assert_image_similar_tofile( + im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 + ) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or CBDT support") - assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) - except OSError as e: # pragma: no cover - assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or SBIX support") - @skip_unless_feature_version("freetype2", "2.10.0") - def test_colr(self): +def test_sbix(layout_engine): + try: font = ImageFont.truetype( - "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", - size=64, - layout_engine=self.LAYOUT_ENGINE, + "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine ) - im = Image.new("RGB", (300, 75), "white") + im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) - d.text((15, 5), "Bungee", font=font, embedded_color=True) + d.text((50, 50), "\uE901", font=font, embedded_color=True) - assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") - @skip_unless_feature_version("freetype2", "2.10.0") - def test_colr_mask(self): + +def test_sbix_mask(layout_engine): + try: font = ImageFont.truetype( - "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", - size=64, - layout_engine=self.LAYOUT_ENGINE, + "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine ) - im = Image.new("RGB", (300, 75), "white") + im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) - d.text((15, 5), "Bungee", "black", font=font) + d.text((50, 50), "\uE901", (100, 0, 0), font=font) + + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) + except OSError as e: # pragma: no cover + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or SBIX support") + + +@skip_unless_feature_version("freetype2", "2.10.0") +def test_colr(layout_engine): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "Bungee", font=font, embedded_color=True) + + assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) + + +@skip_unless_feature_version("freetype2", "2.10.0") +def test_colr_mask(layout_engine): + font = ImageFont.truetype( + "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", + size=64, + layout_engine=layout_engine, + ) + + im = Image.new("RGB", (300, 75), "white") + d = ImageDraw.Draw(im) - assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) + d.text((15, 5), "Bungee", "black", font=font) - def test_fill_deprecation(self): - font = self.get_font() - with pytest.warns(DeprecationWarning): - font.getmask2("Hello world", fill=Image.core.fill) - with pytest.warns(DeprecationWarning): - with pytest.raises(TypeError): - font.getmask2("Hello world", fill=None) + assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) -@skip_unless_feature("raqm") -class TestImageFont_RaqmLayout(TestImageFont): - LAYOUT_ENGINE = ImageFont.Layout.RAQM +def test_fill_deprecation(font): + with pytest.warns(DeprecationWarning): + font.getmask2("Hello world", fill=Image.core.fill) + with pytest.warns(DeprecationWarning): + with pytest.raises(TypeError): + font.getmask2("Hello world", fill=None) def test_render_mono_size(): From 5a38c7f95357e29b05edadcfd86a78eec1cc6ed9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Aug 2022 13:05:21 +1000 Subject: [PATCH 169/242] Updated libimagequant to 4.0.4 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 76f4cb95f3f..64dd024bd7f 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.2 +archive=libimagequant-4.0.4 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index a8cd5e4415a..bb547c1adaa 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -166,7 +166,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.2** + * Pillow has been tested with libimagequant **2.6-4.0.4** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From ac83011fbf91341ec50761784a8d4e4c5baae9f2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Aug 2022 18:09:18 +1000 Subject: [PATCH 170/242] NumPy now supports Python 3.11 --- .ci/install.sh | 3 +-- .github/workflows/macos-install.sh | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 7ead209bec2..518b66acc23 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -37,8 +37,7 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - # TODO Remove condition when NumPy supports 3.11 - if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi + python3 -m pip install numpy # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index bb0bcd6803e..65f2b81d543 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -14,8 +14,7 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg -# TODO Remove condition when NumPy supports 3.11 -if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi +python3 -m pip install numpy # extra test images pushd depends && ./install_extra_test_images.sh && popd From 38b53a9fd704570fb29abd10910ea7939b1185e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Aug 2022 20:33:51 +1000 Subject: [PATCH 171/242] Do not call load() before draft() --- Tests/test_image_thumbnail.py | 22 ++++++++++++++++++ src/PIL/Image.py | 42 ++++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 20cc101ed4e..4fd07a2b4d2 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -97,6 +97,28 @@ def test_load_first(): im.thumbnail((64, 64)) assert im.size == (64, 10) + # Test thumbnail(), without draft(), + # on an image that is large enough once load() has changed the size + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im.thumbnail((590, 88), reducing_gap=None) + assert im.size == (590, 88) + + +def test_load_first_unless_jpeg(): + # Test that thumbnail() still uses draft() for JPEG + with Image.open("Tests/images/hopper.jpg") as im: + draft = im.draft + + def im_draft(mode, size): + result = draft(mode, size) + assert result is not None + + return result + + im.draft = im_draft + + im.thumbnail((64, 64)) + # valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4eb2dead655..afe1feedec8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2473,29 +2473,41 @@ def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): :returns: None """ - self.load() - x, y = map(math.floor, size) - if x >= self.width and y >= self.height: - return + provided_size = tuple(map(math.floor, size)) - def round_aspect(number, key): - return max(min(math.floor(number), math.ceil(number), key=key), 1) + def preserve_aspect_ratio(): + def round_aspect(number, key): + return max(min(math.floor(number), math.ceil(number), key=key), 1) - # preserve aspect ratio - aspect = self.width / self.height - if x / y >= aspect: - x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) - else: - y = round_aspect( - x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) - ) - size = (x, y) + x, y = provided_size + if x >= self.width and y >= self.height: + return + + aspect = self.width / self.height + if x / y >= aspect: + x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) + else: + y = round_aspect( + x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) + ) + return x, y box = None if reducing_gap is not None: + size = preserve_aspect_ratio() + if size is None: + return + res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) if res is not None: box = res[1] + if box is None: + self.load() + + # load() may have changed the size of the image + size = preserve_aspect_ratio() + if size is None: + return if self.size != size: im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) From e58b1960c34185c682fe9108934b91dcd34ee208 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Aug 2022 22:48:12 +1000 Subject: [PATCH 172/242] Set top-level permissions for remaining GitHub Actions --- .github/workflows/cifuzz.yml | 3 +++ .github/workflows/test-cygwin.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 0e0abaf95ee..fa1e8a50309 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -11,6 +11,9 @@ on: - "**.h" workflow_dispatch: +permissions: + contents: read + jobs: Fuzzing: runs-on: ubuntu-latest diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 417b1f21276..794159cec67 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -2,6 +2,9 @@ name: Test Cygwin on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: build: runs-on: windows-latest From 2d21bc06f3d482286be4998e24cfcafe780aff2b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 27 Aug 2022 12:17:40 +1000 Subject: [PATCH 173/242] Replaced Codecov bash uploader with GitHub Action --- .github/workflows/test.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d41f4b57196..77d0dcc2438 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,11 +27,6 @@ jobs: REVERSE: "--reverse" - python-version: "3.8" PYTHONOPTIMIZE: 2 - # Include new variables for Codecov - - os: ubuntu-latest - codecov-flag: GHA_Ubuntu - - os: macos-latest - codecov-flag: GHA_macOS runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} @@ -104,9 +99,11 @@ jobs: .ci/after_success.sh - name: Upload coverage - run: bash <(curl -s https://codecov.io/bash) -F ${{ matrix.codecov-flag }} - env: - CODECOV_NAME: ${{ matrix.os }} Python ${{ matrix.python-version }} + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} success: needs: build From e61327177601181d7395fa92f5fad36aeb5b6652 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 Aug 2022 18:48:47 +1000 Subject: [PATCH 174/242] Fixed typo --- src/libImaging/TiffDecode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 3bb444c804c..04a835dcdde 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -916,7 +916,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt dump_state(clientstate); if (state->state == 0) { - TRACE(("Encoding line bt line")); + TRACE(("Encoding line by line")); while (state->y < state->ysize) { state->shuffle( state->buffer, From 9fa421923c7a46827f5f79bca788c44cb57f14c7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 28 Aug 2022 15:58:30 +1000 Subject: [PATCH 175/242] Removed requirement for 256 palette entries --- Tests/test_image.py | 1 + src/PIL/Image.py | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 7cebed127d9..ab945e946f0 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -620,6 +620,7 @@ def test_remap_palette_transparency(self): im_remapped = im.remap_palette([1, 0]) assert im_remapped.info["transparency"] == 1 + assert len(im_remapped.getpalette()) == 6 # Test unused transparency im.info["transparency"] = 2 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4eb2dead655..e197c018240 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1944,11 +1944,7 @@ def remap_palette(self, dest_map, source_palette=None): m_im = m_im.convert("L") - # Internally, we require 256 palette entries. - new_palette_bytes = ( - palette_bytes + ((256 * bands) - len(palette_bytes)) * b"\x00" - ) - m_im.putpalette(new_palette_bytes, palette_mode) + m_im.putpalette(palette_bytes, palette_mode) m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) if "transparency" in self.info: From 599637808cfd762529c7ffc6458776a4efb8d0d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Aug 2022 13:22:09 +1000 Subject: [PATCH 176/242] Documented TGA keyword arguments when saving --- docs/handbook/image-file-formats.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 7db7b117a77..ff54853a337 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -837,6 +837,24 @@ Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``, ``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and run-length encoded TGAs. +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**compression** + If set to "tga_rle", the file will be run-length encoded. + + .. versionadded:: 5.3.0 + +**id_section** + The identification field. + + .. versionadded:: 5.3.0 + +**orientation** + If present and a positive number, the first pixel is for the top left corner, + rather than the bottom left corner. + + .. versionadded:: 5.3.0 + TIFF ^^^^ From e7fab6abf44faf3bdf45b99c239bf38569a1ece4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Aug 2022 23:20:31 +1000 Subject: [PATCH 177/242] Fixed remapping to palette with duplicate entries --- Tests/test_file_gif.py | 13 +++++++++++++ src/PIL/GifImagePlugin.py | 2 ++ 2 files changed, 15 insertions(+) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 68cb8a36e8d..4e967faec91 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1087,6 +1087,19 @@ def test_palette_save_P(tmp_path): assert_image_equal(reloaded, im) +def test_palette_save_duplicate_entries(tmp_path): + im = Image.new("P", (1, 2)) + im.putpixel((0, 1), 1) + + im.putpalette((0, 0, 0, 0, 0, 0)) + + out = str(tmp_path / "temp.gif") + im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) + + with Image.open(out) as reloaded: + assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) + + def test_palette_save_all_P(tmp_path): frames = [] colors = ((255, 0, 0), (0, 255, 0)) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2e11df54c0a..40fbaa9b568 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -523,6 +523,8 @@ def _normalize_palette(im, palette, info): index = im.palette.colors[source_color] except KeyError: index = None + if index in used_palette_colors: + index = None used_palette_colors.append(index) for i, index in enumerate(used_palette_colors): if index is None: From 09a7255cedc0117e530433b689639b37d2b497e0 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 29 Aug 2022 11:35:06 -0500 Subject: [PATCH 178/242] Apply suggestions from code review Co-authored-by: Hugo van Kemenade --- Tests/test_image_filter.py | 2 +- Tests/test_image_reduce.py | 16 ++++++++-------- Tests/test_image_transform.py | 22 +++++++++++----------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index bec7f21e947..07f4d08add8 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -74,7 +74,7 @@ def test_modefilter(mode, expected): @pytest.mark.parametrize( - "mode,expected", + "mode, expected", ( ("1", (0, 4, 8)), ("L", (0, 4, 8)), diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index 90beeeb689c..801161511b9 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -39,7 +39,7 @@ @pytest.mark.parametrize( - "size,expected", + "size, expected", ( (3, (4, 4)), ((3, 1), (4, 10)), @@ -52,16 +52,16 @@ def test_args_factor(size, expected): @pytest.mark.parametrize( - "size,error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) + "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) ) -def test_args_factor_error(size, error): +def test_args_factor_error(size, expected_error): im = Image.new("L", (10, 10)) - with pytest.raises(error): + with pytest.raises(expected_error): im.reduce(size) @pytest.mark.parametrize( - "size,expected", + "size, expected", ( ((0, 0, 10, 10), (5, 5)), ((5, 5, 6, 6), (1, 1)), @@ -73,7 +73,7 @@ def test_args_box(size, expected): @pytest.mark.parametrize( - "size,error", + "size, expected_error", ( ("stri", TypeError), ((0, 0, 11, 10), ValueError), @@ -84,9 +84,9 @@ def test_args_box(size, expected): ((5, 0, 5, 10), ValueError), ), ) -def test_args_box_error(size, error): +def test_args_box_error(size, expected_error): im = Image.new("L", (10, 10)) - with pytest.raises(error): + with pytest.raises(expected_error): im.reduce(2, size).size diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 14ca0334aa9..a78349801fc 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -76,14 +76,14 @@ def test_quad(self): assert_image_equal(transformed, scaled) @pytest.mark.parametrize( - "mode,pixel", + "mode, expected_pixel", ( ("RGB", (255, 0, 0)), ("RGBA", (255, 0, 0, 255)), ("LA", (76, 0)), ), ) - def test_fill(self, mode, pixel): + def test_fill(self, mode, expected_pixel): im = hopper(mode) (w, h) = im.size transformed = im.transform( @@ -93,7 +93,7 @@ def test_fill(self, mode, pixel): Image.Resampling.BILINEAR, fillcolor="red", ) - assert transformed.getpixel((w - 1, h - 1)) == pixel + assert transformed.getpixel((w - 1, h - 1)) == expected_pixel def test_mesh(self): # this should be a checkerboard of halfsized hoppers in ul, lr @@ -240,7 +240,7 @@ def _test_image(self): return im.crop((10, 20, im.width - 10, im.height - 20)) @pytest.mark.parametrize( - "deg,transpose", + "deg, transpose", ( (0, None), (90, Image.Transpose.ROTATE_90), @@ -281,7 +281,7 @@ def test_rotate(self, deg, transpose): assert_image_equal(transposed, transformed) @pytest.mark.parametrize( - "scale,epsilonscale", + "scale, epsilon_scale", ( (1.1, 6.9), (1.5, 5.5), @@ -298,7 +298,7 @@ def test_rotate(self, deg, transpose): (Image.Resampling.BICUBIC, 1), ), ) - def test_resize(self, scale, epsilonscale, resample, epsilon): + def test_resize(self, scale, epsilon_scale, resample, epsilon): im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) @@ -309,10 +309,10 @@ def test_resize(self, scale, epsilonscale, resample, epsilon): transformed = transformed.transform( im.size, self.transform, matrix_down, resample ) - assert_image_similar(transformed, im, epsilon * epsilonscale) + assert_image_similar(transformed, im, epsilon * epsilon_scale) @pytest.mark.parametrize( - "x,y,epsilonscale", + "x, y, epsilon_scale", ( (0.1, 0, 3.7), (0.6, 0, 9.1), @@ -320,14 +320,14 @@ def test_resize(self, scale, epsilonscale, resample, epsilon): ), ) @pytest.mark.parametrize( - "resample,epsilon", + "resample, epsilon", ( (Image.Resampling.NEAREST, 0), (Image.Resampling.BILINEAR, 1.5), (Image.Resampling.BICUBIC, 1), ), ) - def test_translate(self, x, y, epsilonscale, resample, epsilon): + def test_translate(self, x, y, epsilon_scale, resample, epsilon): im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) @@ -338,7 +338,7 @@ def test_translate(self, x, y, epsilonscale, resample, epsilon): transformed = transformed.transform( im.size, self.transform, matrix_down, resample ) - assert_image_similar(transformed, im, epsilon * epsilonscale) + assert_image_similar(transformed, im, epsilon * epsilon_scale) class TestImageTransformPerspective(TestImageTransformAffine): From 797eb397115c971f785414e6a89f25e7903ae853 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 29 Aug 2022 12:28:14 -0500 Subject: [PATCH 179/242] Apply suggestions from code review Co-authored-by: Hugo van Kemenade --- Tests/test_image_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 07f4d08add8..cfe46b65898 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -52,7 +52,7 @@ def test_crash(size): @pytest.mark.parametrize( - "mode,expected", + "mode, expected", ( ("1", (4, 0)), ("L", (4, 0)), From 0ec3d3ec2c0e12fdd98caeac47bf1bcfe2be7701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Mon, 29 Aug 2022 20:34:11 +0200 Subject: [PATCH 180/242] Use pytest.param for consistency Co-authored-by: Hugo van Kemenade --- Tests/test_imagefont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f8ecc193a7c..09e5370e2d4 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -35,7 +35,7 @@ def test_sanity(): @pytest.fixture( scope="module", params=[ - ImageFont.Layout.BASIC, + pytest.param(ImageFont.Layout.BASIC), pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), ], ) From 841ba4a940b2b09b5c07a43c7a75ce1266d0f2c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Aug 2022 08:08:01 +1000 Subject: [PATCH 181/242] Simplified code --- src/PIL/GifImagePlugin.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 40fbaa9b568..20435fe313a 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -519,10 +519,7 @@ def _normalize_palette(im, palette, info): used_palette_colors = [] for i in range(0, len(source_palette), 3): source_color = tuple(source_palette[i : i + 3]) - try: - index = im.palette.colors[source_color] - except KeyError: - index = None + index = im.palette.colors.get(source_color) if index in used_palette_colors: index = None used_palette_colors.append(index) From 7b0e56bb211ab5880d08b5cc159c9744c34601a8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Aug 2022 09:21:24 +1000 Subject: [PATCH 182/242] Removed support for Python before interpaddr() --- src/PIL/ImageTk.py | 23 ++++++++++------------- src/_imagingtk.c | 21 ++------------------- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index c2c4d774c59..33c0cdacc93 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -68,21 +68,18 @@ def _pyimagingtkcall(command, photo, id): # may raise an error if it cannot attach to Tkinter from . import _imagingtk - try: - if hasattr(tk, "interp"): - # Required for PyPy, which always has CFFI installed - from cffi import FFI + if hasattr(tk, "interp"): + # Required for PyPy, which always has CFFI installed + from cffi import FFI - ffi = FFI() + ffi = FFI() - # PyPy is using an FFI CDATA element - # (Pdb) self.tk.interp - # - _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1) - else: - _imagingtk.tkinit(tk.interpaddr(), 1) - except AttributeError: - _imagingtk.tkinit(id(tk), 0) + # PyPy is using an FFI CDATA element + # (Pdb) self.tk.interp + # + _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp))) + else: + _imagingtk.tkinit(tk.interpaddr()) tk.call(command, photo, id) diff --git a/src/_imagingtk.c b/src/_imagingtk.c index 3f154166b9d..b9273b0b882 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -23,33 +23,16 @@ TkImaging_Init(Tcl_Interp *interp); extern int load_tkinter_funcs(void); -/* copied from _tkinter.c (this isn't as bad as it may seem: for new - versions, we use _tkinter's interpaddr hook instead, and all older - versions use this structure layout) */ - -typedef struct { - PyObject_HEAD Tcl_Interp *interp; -} TkappObject; - static PyObject * _tkinit(PyObject *self, PyObject *args) { Tcl_Interp *interp; PyObject *arg; - int is_interp; - if (!PyArg_ParseTuple(args, "Oi", &arg, &is_interp)) { + if (!PyArg_ParseTuple(args, "O", &arg)) { return NULL; } - if (is_interp) { - interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); - } else { - TkappObject *app; - /* Do it the hard way. This will break if the TkappObject - layout changes */ - app = (TkappObject *)PyLong_AsVoidPtr(arg); - interp = app->interp; - } + interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); /* This will bomb if interp is invalid... */ TkImaging_Init(interp); From d6e59bc750c0649ed26fc71a83e86e17ea862ec3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Aug 2022 19:41:14 +1000 Subject: [PATCH 183/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fb634eabad1..ffb1dc06bd3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Removed support for tkinter before Python 1.5.2 #6549 + [radarhere] + - Allow default ImageDraw font to be set #6484 [radarhere, hugovk] From 172f1f3369c318338a49e584028640b34a0a475f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Aug 2022 20:30:58 +1000 Subject: [PATCH 184/242] Updated environment list [ci skip] --- winbuild/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index 611d1ed1a89..d8538fbf392 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,8 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires CMake 3.12 or newer (available as Visual Studio component). -* Tested on Windows Server 2016 with Visual Studio 2017 Community (AppVeyor). -* Tested on Windows Server 2019 with Visual Studio 2019 Enterprise (GitHub Actions). +* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). +* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). The following is a simplified version of the script used on AppVeyor: ``` From 54c560f6119dd492ed251a3deb3ba67b4b05b2be Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 30 Aug 2022 14:12:48 +0200 Subject: [PATCH 185/242] Removed support for PyPy before Python 3.6 --- src/PIL/ImageTk.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 33c0cdacc93..7c90a0ad893 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -68,18 +68,7 @@ def _pyimagingtkcall(command, photo, id): # may raise an error if it cannot attach to Tkinter from . import _imagingtk - if hasattr(tk, "interp"): - # Required for PyPy, which always has CFFI installed - from cffi import FFI - - ffi = FFI() - - # PyPy is using an FFI CDATA element - # (Pdb) self.tk.interp - # - _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp))) - else: - _imagingtk.tkinit(tk.interpaddr()) + _imagingtk.tkinit(tk.interpaddr()) tk.call(command, photo, id) From 196210bc804a4575b6cc0b1cb6c9b7b1f09e4ff9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Aug 2022 08:10:35 +1000 Subject: [PATCH 186/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ffb1dc06bd3..63c71cd0fc7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Open 1 bit EPS in mode 1 #6499 + [radarhere] + - Removed support for tkinter before Python 1.5.2 #6549 [radarhere] From b3dcf17886dcb4a6c392c83eec4f393d7b4efaca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Aug 2022 20:09:05 +1000 Subject: [PATCH 187/242] Use constants --- src/PIL/TiffImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index da33cc5a501..c70ed333c1a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1153,7 +1153,7 @@ def getxmp(self): :returns: XMP tags in a dictionary. """ - return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {} + return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {} def get_photoshop_blocks(self): """ @@ -1328,7 +1328,7 @@ def _setup(self): logger.debug(f"- photometric_interpretation: {photo}") logger.debug(f"- planar_configuration: {self._planar_configuration}") logger.debug(f"- fill_order: {fillorder}") - logger.debug(f"- YCbCr subsampling: {self.tag.get(530)}") + logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}") # size xsize = int(self.tag_v2.get(IMAGEWIDTH)) @@ -1469,8 +1469,8 @@ def _setup(self): else: # tiled image offsets = self.tag_v2[TILEOFFSETS] - w = self.tag_v2.get(322) - h = self.tag_v2.get(323) + w = self.tag_v2.get(TILEWIDTH) + h = self.tag_v2.get(TILELENGTH) for offset in offsets: if x + w > xsize: From 96c4f5401209ef7254029e97059fd9d2aaa86b69 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Aug 2022 21:03:21 +1000 Subject: [PATCH 188/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 63c71cd0fc7..7157f12ef5c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Do not use CCITTFaxDecode filter if libtiff is not available #6518 + [radarhere] + +- Fallback to not using mmap if buffer is not large enough #6510 + [radarhere] + +- Fixed writing bytes as ASCII tag #6493 + [radarhere] + - Open 1 bit EPS in mode 1 #6499 [radarhere] From 06660a5bad41dbaf9a73410372fe75852c8a7c5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Aug 2022 21:29:27 +1000 Subject: [PATCH 189/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7157f12ef5c..012c6e8cd55 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Removed support for tkinter in PyPy before Python 3.6 #6551 + [nulano] + - Do not use CCITTFaxDecode filter if libtiff is not available #6518 [radarhere] From 3f960d9a94e5f2cd789da8b9fd0d1d6db8a60cba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Sep 2022 08:37:15 +1000 Subject: [PATCH 190/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 012c6e8cd55..67d1500058f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Copy palette when converting from P to PA #6497 + [radarhere] + +- Allow RGB and RGBA values for PA image putpixel #6504 + [radarhere] + - Removed support for tkinter in PyPy before Python 3.6 #6551 [nulano] From 7966c344ac4cc572edf1fc19246e650547b76cb6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Sep 2022 22:26:35 +1000 Subject: [PATCH 191/242] Improved documentation of return values --- docs/reference/ImageDraw.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 1ef9079fba0..75ef0f5cc9b 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -489,6 +489,8 @@ Methods .. versionadded:: 6.2.0 + :returns: A ``(width, height)`` tuple. + .. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) .. deprecated:: 9.2.0 @@ -541,6 +543,8 @@ Methods .. versionadded:: 6.2.0 + :returns: A ``(width, height)`` tuple. + .. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False) Returns length (in pixels with 1/64 precision) of given text when rendered @@ -608,6 +612,7 @@ Methods It should be a `BCP 47 language code`_. Requires libraqm. :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + :returns: A single float, the length of the text. .. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) @@ -657,6 +662,8 @@ Methods Requires libraqm. :param stroke_width: The width of the text stroke. :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + :returns: An ``(x0, y0, x1, y1)`` tuple, describing the top left and lower right + corners of the text. .. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) @@ -700,6 +707,8 @@ Methods Requires libraqm. :param stroke_width: The width of the text stroke. :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). + :returns: An ``(x0, y0, x1, y1)`` tuple, describing the top left and lower right + corners of the text. .. py:method:: getdraw(im=None, hints=None) From a36b766d3658edff41a80f65fb88295640a3d9a3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Sep 2022 20:53:22 +1000 Subject: [PATCH 192/242] Simplified enum references --- docs/reference/Image.rst | 8 ++--- src/PIL/Image.py | 72 +++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ed37521fdae..7f6f666c33c 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -53,9 +53,9 @@ Functions To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an - image is over a certain limit, :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. + image is over a certain limit, :py:data:`MAX_IMAGE_PIXELS`. - This threshold can be changed by setting :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. It can be disabled + This threshold can be changed by setting :py:data:`MAX_IMAGE_PIXELS`. It can be disabled by setting ``Image.MAX_IMAGE_PIXELS = None``. If desired, the warning can be turned into an error with @@ -63,7 +63,7 @@ Functions ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also `the logging documentation`_ to have warnings output to the logging facility instead of stderr. - If the number of pixels is greater than twice :py:data:`PIL.Image.MAX_IMAGE_PIXELS`, then a + If the number of pixels is greater than twice :py:data:`MAX_IMAGE_PIXELS`, then a ``DecompressionBombError`` will be raised instead. .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb @@ -255,7 +255,7 @@ This rotates the input image by ``theta`` degrees counter clockwise: .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose -This flips the input image by using the :data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT` +This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` method. .. code-block:: python diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f3f158db8bf..a958f064db7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1989,18 +1989,14 @@ def resize(self, size, resample=None, box=None, reducing_gap=None): :param size: The requested size in pixels, as a 2-tuple: (width, height). :param resample: An optional resampling filter. This can be - one of :py:data:`PIL.Image.Resampling.NEAREST`, - :py:data:`PIL.Image.Resampling.BOX`, - :py:data:`PIL.Image.Resampling.BILINEAR`, - :py:data:`PIL.Image.Resampling.HAMMING`, - :py:data:`PIL.Image.Resampling.BICUBIC` or - :py:data:`PIL.Image.Resampling.LANCZOS`. + one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. If the image has mode "1" or "P", it is always set to - :py:data:`PIL.Image.Resampling.NEAREST`. - If the image mode specifies a number of bits, such as "I;16", then the - default filter is :py:data:`PIL.Image.Resampling.NEAREST`. - Otherwise, the default filter is - :py:data:`PIL.Image.Resampling.BICUBIC`. See: :ref:`concept-filters`. + :py:data:`Resampling.NEAREST`. If the image mode specifies a number + of bits, such as "I;16", then the default filter is + :py:data:`Resampling.NEAREST`. Otherwise, the default filter is + :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. :param box: An optional 4-tuple of floats providing the source image region to be scaled. The values must be within (0, 0, width, height) rectangle. @@ -2140,12 +2136,12 @@ def rotate( :param angle: In degrees counter clockwise. :param resample: An optional resampling filter. This can be - one of :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour), - :py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`PIL.Image.Resampling.BICUBIC` - (cubic spline interpolation in a 4x4 environment). - If omitted, or if the image has mode "1" or "P", it is - set to :py:data:`PIL.Image.Resampling.NEAREST`. See :ref:`concept-filters`. + one of :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline + interpolation in a 4x4 environment). If omitted, or if the image has + mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. + See :ref:`concept-filters`. :param expand: Optional expansion flag. If true, expands the output image to make it large enough to hold the entire rotated image. If false or omitted, make the output image the same size as the @@ -2452,14 +2448,11 @@ def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): :param size: Requested size. :param resample: Optional resampling filter. This can be one - of :py:data:`PIL.Image.Resampling.NEAREST`, - :py:data:`PIL.Image.Resampling.BOX`, - :py:data:`PIL.Image.Resampling.BILINEAR`, - :py:data:`PIL.Image.Resampling.HAMMING`, - :py:data:`PIL.Image.Resampling.BICUBIC` or - :py:data:`PIL.Image.Resampling.LANCZOS`. - If omitted, it defaults to :py:data:`PIL.Image.Resampling.BICUBIC`. - (was :py:data:`PIL.Image.Resampling.NEAREST` prior to version 2.5.0). + of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, + :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, + :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + If omitted, it defaults to :py:data:`Resampling.BICUBIC`. + (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). See: :ref:`concept-filters`. :param reducing_gap: Apply optimization by resizing the image in two steps. First, reducing the image by integer times @@ -2530,11 +2523,11 @@ def transform( :param size: The output size. :param method: The transformation method. This is one of - :py:data:`PIL.Image.Transform.EXTENT` (cut out a rectangular subregion), - :py:data:`PIL.Image.Transform.AFFINE` (affine transform), - :py:data:`PIL.Image.Transform.PERSPECTIVE` (perspective transform), - :py:data:`PIL.Image.Transform.QUAD` (map a quadrilateral to a rectangle), or - :py:data:`PIL.Image.Transform.MESH` (map a number of source quadrilaterals + :py:data:`Transform.EXTENT` (cut out a rectangular subregion), + :py:data:`Transform.AFFINE` (affine transform), + :py:data:`Transform.PERSPECTIVE` (perspective transform), + :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or + :py:data:`Transform.MESH` (map a number of source quadrilaterals in one operation). It may also be an :py:class:`~PIL.Image.ImageTransformHandler` @@ -2554,11 +2547,11 @@ def getdata(self): return method, data :param data: Extra data to the transformation method. :param resample: Optional resampling filter. It can be one of - :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour), - :py:data:`PIL.Image.Resampling.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline + :py:data:`Resampling.NEAREST` (use nearest neighbour), + :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 + environment), or :py:data:`Resampling.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image - has mode "1" or "P", it is set to :py:data:`PIL.Image.Resampling.NEAREST`. + has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. See: :ref:`concept-filters`. :param fill: If ``method`` is an :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of @@ -2685,13 +2678,10 @@ def transpose(self, method): """ Transpose image (flip or rotate in 90 degree steps) - :param method: One of :py:data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`, - :py:data:`PIL.Image.Transpose.FLIP_TOP_BOTTOM`, - :py:data:`PIL.Image.Transpose.ROTATE_90`, - :py:data:`PIL.Image.Transpose.ROTATE_180`, - :py:data:`PIL.Image.Transpose.ROTATE_270`, - :py:data:`PIL.Image.Transpose.TRANSPOSE` or - :py:data:`PIL.Image.Transpose.TRANSVERSE`. + :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, + :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, + :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, + :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. :returns: Returns a flipped or rotated copy of this image. """ From 4783ecf41c1413a05e70d325268141fce6788ac0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Sep 2022 21:09:44 +1000 Subject: [PATCH 193/242] Updated return values to match ImageFont --- docs/reference/ImageDraw.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 75ef0f5cc9b..e19e87a0d52 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -489,7 +489,7 @@ Methods .. versionadded:: 6.2.0 - :returns: A ``(width, height)`` tuple. + :return: (width, height) .. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) @@ -543,7 +543,7 @@ Methods .. versionadded:: 6.2.0 - :returns: A ``(width, height)`` tuple. + :return: (width, height) .. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False) @@ -612,7 +612,7 @@ Methods It should be a `BCP 47 language code`_. Requires libraqm. :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - :returns: A single float, the length of the text. + :return: Width for horizontal, height for vertical text. .. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) @@ -662,8 +662,7 @@ Methods Requires libraqm. :param stroke_width: The width of the text stroke. :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - :returns: An ``(x0, y0, x1, y1)`` tuple, describing the top left and lower right - corners of the text. + :return: ``(left, top, right, bottom)`` bounding box .. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) @@ -707,8 +706,7 @@ Methods Requires libraqm. :param stroke_width: The width of the text stroke. :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - :returns: An ``(x0, y0, x1, y1)`` tuple, describing the top left and lower right - corners of the text. + :return: ``(left, top, right, bottom)`` bounding box .. py:method:: getdraw(im=None, hints=None) From 780de80e5c56e8466faec54f8c0ebe95e543e9ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 3 Sep 2022 22:23:05 +1000 Subject: [PATCH 194/242] Added examples for updating code --- docs/deprecations.rst | 34 ++++++++++++++++++++++++++++++++++ docs/reference/ImageDraw.rst | 6 ++++++ docs/releasenotes/9.2.0.rst | 34 ++++++++++++++++++++++++++++++++++ src/PIL/ImageFont.py | 15 +++++++++++++++ 4 files changed, 89 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 9be92770ab9..92f116846f7 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -197,6 +197,40 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= +Previous code: + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + width, height = font.getsize("Hello world") + left, top = font.getoffset("Hello world") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world") + + width, height = font.getsize_multiline("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld") + +Use instead: + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + left, top, right, bottom = font.getbbox("Hello world") + width, height = right - left, bottom - top + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width = draw.textlength("Hello world") + + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + width, height = right - left, bottom - top + Removed features ---------------- diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index e19e87a0d52..623f601efeb 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -443,6 +443,9 @@ Methods .. deprecated:: 9.2.0 + See https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#font-size-and-offset-methods + for more information. + Use :py:meth:`textlength()` to measure the offset of following text with 1/64 pixel precision. Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. @@ -495,6 +498,9 @@ Methods .. deprecated:: 9.2.0 + See https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#font-size-and-offset-methods + for more information. + Use :py:meth:`.multiline_textbbox` instead. Return the size of the given string, in pixels. diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 9c102f1776a..6dbfa2702eb 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -59,6 +59,40 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= +Previous code: + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + width, height = font.getsize("Hello world") + left, top = font.getoffset("Hello world") + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width, height = draw.textsize("Hello world") + + width, height = font.getsize_multiline("Hello\nworld") + width, height = draw.multiline_textsize("Hello\nworld") + +Use instead: + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + left, top, right, bottom = font.getbbox("Hello world") + width, height = right - left, bottom - top + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + width = draw.textlength("Hello world") + + left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") + width, height = right - left, bottom - top + API Additions ============= diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9386d008602..3d1e4b0bfe1 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -139,6 +139,9 @@ def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 + For more information, see + https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. Returns width and height (in pixels) of given text. @@ -428,6 +431,9 @@ def getsize( """ .. deprecated:: 9.2.0 + For more information, see + https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + Use :py:meth:`getlength()` to measure the offset of following text with 1/64 pixel precision. Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. @@ -498,6 +504,9 @@ def getsize_multiline( """ .. deprecated:: 9.2.0 + For more information, see + https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + Use :py:meth:`.ImageDraw.multiline_textbbox` instead. Returns width and height (in pixels) of given text if rendered in font @@ -557,6 +566,9 @@ def getoffset(self, text): """ .. deprecated:: 9.2.0 + For more information, see + https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + Use :py:meth:`.getbbox` instead. Returns the offset of given text. This is the gap between the @@ -851,6 +863,9 @@ def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 + For more information, see + https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. """ deprecate("getsize", 10, "getbbox or getlength") From ee5de25f8791659fdf43110404dda8908a6a79f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Sep 2022 11:58:45 +1000 Subject: [PATCH 195/242] Apply transparency to P images before passing to tkinter.PhotoImage --- Tests/test_imagetk.py | 7 +++++++ src/PIL/ImageTk.py | 1 + 2 files changed, 8 insertions(+) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index a929910b3cc..a848c786f04 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -69,6 +69,13 @@ def test_photoimage(): assert_image_equal(reloaded, im.convert("RGBA")) +def test_photoimage_apply_transparency(): + with Image.open("Tests/images/pil123p.png") as im: + im_tk = ImageTk.PhotoImage(im) + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded, im.convert("RGBA")) + + def test_photoimage_blank(): # test a image using mode/size: for mode in TK_MODES: diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 7c90a0ad893..949cf1fbf9d 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -107,6 +107,7 @@ def __init__(self, image=None, size=None, **kw): mode = image.mode if mode == "P": # palette mapped data + image.apply_transparency() image.load() try: mode = image.palette.mode From b3683c3e4b58f812c025adbe78790cc6e2e7fa2f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:09:19 +0000 Subject: [PATCH 196/242] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0) - [github.com/asottile/yesqa: v1.3.0 → v1.4.0](https://github.com/asottile/yesqa/compare/v1.3.0...v1.4.0) - [github.com/Lucas-C/pre-commit-hooks: v1.3.0 → v1.3.1](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.3.0...v1.3.1) - [github.com/PyCQA/flake8: 5.0.2 → 5.0.4](https://github.com/PyCQA/flake8/compare/5.0.2...5.0.4) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1bb71bd72ad..eeb4b391ee3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black args: ["--target-version", "py37"] @@ -14,18 +14,18 @@ repos: - id: isort - repo: https://github.com/asottile/yesqa - rev: v1.3.0 + rev: v1.4.0 hooks: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.3.0 + rev: v1.3.1 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) - repo: https://github.com/PyCQA/flake8 - rev: 5.0.2 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] From 209ec9da470ccee20e23620e77972be14016dd7c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Sep 2022 06:43:52 +1000 Subject: [PATCH 197/242] Use target --- docs/deprecations.rst | 2 ++ docs/reference/ImageDraw.rst | 6 ++---- src/PIL/ImageFont.py | 15 +++++---------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 92f116846f7..05de19c9e0f 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -178,6 +178,8 @@ Image.coerce_e This undocumented method has been deprecated and will be removed in Pillow 10 (2023-07-01). +.. _Font size and offset methods: + Font size and offset methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 623f601efeb..61242b35a0c 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -443,8 +443,7 @@ Methods .. deprecated:: 9.2.0 - See https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#font-size-and-offset-methods - for more information. + See :ref:`deprecations ` for more information. Use :py:meth:`textlength()` to measure the offset of following text with 1/64 pixel precision. @@ -498,8 +497,7 @@ Methods .. deprecated:: 9.2.0 - See https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#font-size-and-offset-methods - for more information. + See :ref:`deprecations ` for more information. Use :py:meth:`.multiline_textbbox` instead. diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 3d1e4b0bfe1..c1b7435f363 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -139,8 +139,7 @@ def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 - For more information, see - https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + See :ref:`deprecations ` for more information. Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. @@ -431,8 +430,7 @@ def getsize( """ .. deprecated:: 9.2.0 - For more information, see - https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + See :ref:`deprecations ` for more information. Use :py:meth:`getlength()` to measure the offset of following text with 1/64 pixel precision. @@ -504,8 +502,7 @@ def getsize_multiline( """ .. deprecated:: 9.2.0 - For more information, see - https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + See :ref:`deprecations ` for more information. Use :py:meth:`.ImageDraw.multiline_textbbox` instead. @@ -566,8 +563,7 @@ def getoffset(self, text): """ .. deprecated:: 9.2.0 - For more information, see - https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + See :ref:`deprecations ` for more information. Use :py:meth:`.getbbox` instead. @@ -863,8 +859,7 @@ def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 - For more information, see - https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#deprecations. + See :ref:`deprecations ` for more information. Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. """ From 7359af91f05e72a20b35ae62cc0cdc95e5424ea1 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 6 Sep 2022 16:18:55 +1000 Subject: [PATCH 198/242] Rearranged text Co-authored-by: Hugo van Kemenade --- src/PIL/ImageFont.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c1b7435f363..4df89755be9 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -430,12 +430,12 @@ def getsize( """ .. deprecated:: 9.2.0 - See :ref:`deprecations ` for more information. - Use :py:meth:`getlength()` to measure the offset of following text with 1/64 pixel precision. Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. + See :ref:`deprecations ` for more information. + Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language. @@ -502,10 +502,10 @@ def getsize_multiline( """ .. deprecated:: 9.2.0 - See :ref:`deprecations ` for more information. - Use :py:meth:`.ImageDraw.multiline_textbbox` instead. + See :ref:`deprecations ` for more information. + Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language, while respecting newline characters. @@ -563,10 +563,10 @@ def getoffset(self, text): """ .. deprecated:: 9.2.0 - See :ref:`deprecations ` for more information. - Use :py:meth:`.getbbox` instead. + See :ref:`deprecations ` for more information. + Returns the offset of given text. This is the gap between the starting coordinate and the first marking. Note that this gap is included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`. @@ -859,9 +859,9 @@ def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 - See :ref:`deprecations ` for more information. - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. + + See :ref:`deprecations ` for more information. """ deprecate("getsize", 10, "getbbox or getlength") with warnings.catch_warnings(): From bce9df62f1dd17c93c2615a24b34b22f2688cfac Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 6 Sep 2022 16:19:47 +1000 Subject: [PATCH 199/242] Rearranged text Co-authored-by: Hugo van Kemenade --- src/PIL/ImageFont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 4df89755be9..04da64b9f46 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -139,10 +139,10 @@ def getsize(self, text, *args, **kwargs): """ .. deprecated:: 9.2.0 - See :ref:`deprecations ` for more information. - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. + See :ref:`deprecations ` for more information. + Returns width and height (in pixels) of given text. :param text: Text to measure. From 9ebf44f8b482954ea3e4c97f4dfcc230bde5c08a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Sep 2022 14:24:11 +0000 Subject: [PATCH 200/242] Add renovate.json --- renovate.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000000..f9c2c327040 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} From bc069ec93901d9879b6768094529e3804d39d063 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Sep 2022 08:37:45 +1000 Subject: [PATCH 201/242] Added renovate.json to MANIFEST --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 26f9401f2d9..08f6dfc0877 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -25,6 +25,7 @@ exclude .coveragerc exclude .editorconfig exclude .readthedocs.yml exclude codecov.yml +exclude renovate.json global-exclude .git* global-exclude *.pyc global-exclude *.so From 01657d128d063d5a848ef68d28df5d3ea5579208 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Sep 2022 20:19:13 +0300 Subject: [PATCH 202/242] Add label to Dependabot PRs --- renovate.json => .github/renovate.json | 3 +++ .pre-commit-config.yaml | 1 + 2 files changed, 4 insertions(+) rename renovate.json => .github/renovate.json (72%) diff --git a/renovate.json b/.github/renovate.json similarity index 72% rename from renovate.json rename to .github/renovate.json index f9c2c327040..9fd3341b430 100644 --- a/renovate.json +++ b/.github/renovate.json @@ -2,5 +2,8 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" + ], + "labels": [ + "Dependency" ] } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eeb4b391ee3..f81bcb956fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,6 +40,7 @@ repos: rev: v4.3.0 hooks: - id: check-merge-conflict + - id: check-json - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint From ae833dd62de4196ec82b5a10ce4daec798cd4a99 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Sep 2022 20:19:37 +0300 Subject: [PATCH 203/242] Group GHA updates into a single PR --- .github/renovate.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 9fd3341b430..cc8f0225f30 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -5,5 +5,12 @@ ], "labels": [ "Dependency" - ] + ], + "packageRules": [ + { + "groupName": "github-actions", + "matchManagers": ["github-actions"], + "separateMajorMinor": "false" + } + ] } From a7471e9b843f267befea41d64bb1e1c4cce9a554 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Sep 2022 20:19:56 +0300 Subject: [PATCH 204/242] Create update PRs on the first day of the month --- .github/renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index cc8f0225f30..4341752c3ae 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -12,5 +12,6 @@ "matchManagers": ["github-actions"], "separateMajorMinor": "false" } - ] + ], + "schedule": ["on the first day of the month"] } From 2a7e603ae2950f04a4b032e6763fffa2a0cb21a8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Sep 2022 22:28:30 +1000 Subject: [PATCH 205/242] Defer parsing of palette into colors --- src/PIL/ImagePalette.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 853147ac28f..b73b2cd9dd0 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -50,15 +50,24 @@ def palette(self): @palette.setter def palette(self, palette): + self._colors = None self._palette = palette - mode_len = len(self.mode) - self.colors = {} - for i in range(0, len(self.palette), mode_len): - color = tuple(self.palette[i : i + mode_len]) - if color in self.colors: - continue - self.colors[color] = i // mode_len + @property + def colors(self): + if self._colors is None: + mode_len = len(self.mode) + self._colors = {} + for i in range(0, len(self.palette), mode_len): + color = tuple(self.palette[i : i + mode_len]) + if color in self._colors: + continue + self._colors[color] = i // mode_len + return self._colors + + @colors.setter + def colors(self, colors): + self._colors = colors def copy(self): new = ImagePalette() From f21bc40b236281b69192d90f215b543b032841ff Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 9 Sep 2022 16:24:11 +0300 Subject: [PATCH 206/242] Avoid release days to keep the CI free Can be 1st, 2nd or 15th: https://github.com/python-pillow/Pillow/blob/main/RELEASING.md --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 4341752c3ae..e378ffc7877 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -13,5 +13,5 @@ "separateMajorMinor": "false" } ], - "schedule": ["on the first day of the month"] + "schedule": ["on the third day of the month"] } From beb7b4d0f6db44dcae71a02c6dd4f3c39da03992 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Sep 2022 22:50:54 +1000 Subject: [PATCH 207/242] Added reading of TIFF child images --- Tests/images/child_ifd.tiff | Bin 0 -> 2971 bytes Tests/images/child_ifd_jpeg.tiff | Bin 0 -> 830 bytes Tests/test_file_tiff.py | 17 ++++++++++++++++ src/PIL/TiffImagePlugin.py | 33 +++++++++++++++++++++++++++++++ src/PIL/TiffTags.py | 1 + 5 files changed, 51 insertions(+) create mode 100644 Tests/images/child_ifd.tiff create mode 100644 Tests/images/child_ifd_jpeg.tiff diff --git a/Tests/images/child_ifd.tiff b/Tests/images/child_ifd.tiff new file mode 100644 index 0000000000000000000000000000000000000000..700185d88ae6b504dfef29689573fe6afcb4ff73 GIT binary patch literal 2971 zcmeH|NlX(_7{}kto2{jqPH91r#8N;jA;wZf@xZ2tgamLwx#6w{ z6A;&nLB;K0L~$b?1UJBosNfP0;98BQe%~;oH8fT{m_Yd2@6CVaecyZUdvAWN%N0W; zLWqeFh9e9pD>&n$S%wk{&-%bAXp9lT1yu{6$1^Mmh3i{zSCBtg`Pf*=J+Iz1XFLy+Ep zH#o>(D$7&K>qS9SLWb3NuM$}zR$`@$jj%F9Wyae|Cc`>;WqG(OiZLwZctM0^1VKQv z5tdIWESiTELv#i76Hew$5snlgI5S(c6`JF!>guJ))TS0oR(pSxGpDFprw<98FflB8 z%2fNbnE06qvl3?~rOla}K5xD&W8tF3OO`HMzGC&7T=&|%{B^}88#b11D%-qe>$dGR zJ9gIY+P!D*zWoOd9%?w!c=Xuu6DLodIeV`8{Dq5`E?>LedgJD;+jrVJ?mu|==y7LP zch9ruFM9i4zIy#;;NANVA3uHeeEEuXG4Mib=C>{x>SFN>M68QpOTnqk@evNel#(fC z7Y5tnsw8u2U42uFF4CFRZ^pbg%RKH@&@~QuAbQ7m?wV26;C^UNQkwfJBQ0Sz6$uJ50`Ds8wq?HiE)f?<#%iJ@clc} z_>ZnJjap;+Fx>o!X$!Uiq$jrbD6rTjd%s@6w4I7lr1QZv9s-FR5i$cXg zdc=^-5r?ut;xb6)ctOQy1I0mr0}O!-CI&_(h?91Lcu;mL$UY$Wf8+lH1_3WOPd5fe zppzIu?)rb5!I^=Bjg6g+m4ls~os*M;i${c)hnt&6Qb?FzL{>^(PF6}rMnOeST|r4l zSw=>~TvNxu(8R<ch$n!2CT12^Hg*n9E^eTLtpW_d;AUoGVP<7zVFAk4 z0_7Q41X+a?4ISBp0~6Vm3Pp?>CobercG`GQH0a_772~9$CQdFfaS2H&RW)@DO)V2s zGjj_|D`yv1H+K(Dui%i-u<(e;sN|H?wDgS3tm2Z=vhs?`s^*r~w)T$Bu1S-pOr17; z#>`oZ7B5-4Z25|nt2S-kvUS_`9Xod&I(+2lvEwIBp1O4T%GGPvZ`{1~@X_NZPoF)1 z@$%KjPoKYh{r3IG&tD*aF#=Nt3_yH_<}X2@znEB9m|56C{$gY*2YFnOg;mjzO~^5j zJ+V+&$*7S-#A)KfjR!fEje|ajCKX-e5>qjGsQMA)HL%Z!^H>vEK7)G<;jdc^Jj{&1 c$YT~{uxEJmVo}%6$=?MQb!jj_G4ubM03_ns#{d8T literal 0 HcmV?d00001 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 8706cb950dd..f98b0bc91bf 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -84,6 +84,23 @@ def test_context_manager(self): with Image.open("Tests/images/multipage.tiff") as im: im.load() + def test_get_child_images(self): + def check(ims, sizes): + assert len(ims) == len(sizes) + + for i, im in enumerate(ims): + w = sizes[i] + expected = Image.new("RGB", (w, w), "#f00") + assert_image_similar(im, expected, 1) + + with Image.open("Tests/images/child_ifd.tiff") as im: + ims = im.get_child_images() + check(ims, (16, 8)) + + with Image.open("Tests/images/child_ifd_jpeg.tiff") as im: + ims = im.get_child_images() + check(ims, (20,)) + def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f2f1299122e..766d46ffb23 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1148,6 +1148,39 @@ def tell(self): """Return the current frame number""" return self.__frame + def get_child_images(self): + if SUBIFD not in self.tag_v2: + return [] + child_images = [] + exif = self.getexif() + offset = None + for im_offset in self.tag_v2[SUBIFD]: + # reset buffered io handle in case fp + # was passed to libtiff, invalidating the buffer + current_offset = self._fp.tell() + if offset is None: + offset = current_offset + + fp = self._fp + ifd = exif._get_ifd_dict(im_offset) + jpegInterchangeFormat = ifd.get(513) + if jpegInterchangeFormat is not None: + fp.seek(jpegInterchangeFormat) + jpeg_data = fp.read(ifd.get(514)) + + fp = io.BytesIO(jpeg_data) + + with Image.open(fp) as im: + if jpegInterchangeFormat is None: + im._frame_pos = [im_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + self._fp.seek(offset) + return child_images + def getxmp(self): """ Returns a dictionary containing the XMP tags. diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index e3094b4db47..3f3a1ccd2a7 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -160,6 +160,7 @@ def lookup(tag, group=None): 323: ("TileLength", LONG, 1), 324: ("TileOffsets", LONG, 0), 325: ("TileByteCounts", LONG, 0), + 330: ("SubIFDs", LONG, 0), 332: ("InkSet", SHORT, 1), 333: ("InkNames", ASCII, 1), 334: ("NumberOfInks", SHORT, 1), From ed016f8f5a0d6aed98897bcb992b937a1d9d7d18 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 11 Sep 2022 09:20:45 +1000 Subject: [PATCH 208/242] Parametrized test --- Tests/test_file_tiff.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index f98b0bc91bf..ac0bd7f6096 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -84,22 +84,23 @@ def test_context_manager(self): with Image.open("Tests/images/multipage.tiff") as im: im.load() - def test_get_child_images(self): - def check(ims, sizes): - assert len(ims) == len(sizes) - - for i, im in enumerate(ims): - w = sizes[i] - expected = Image.new("RGB", (w, w), "#f00") - assert_image_similar(im, expected, 1) - - with Image.open("Tests/images/child_ifd.tiff") as im: + @pytest.mark.parametrize( + "path, sizes", + ( + ("Tests/images/hopper.tif", ()), + ("Tests/images/child_ifd.tiff", (16, 8)), + ("Tests/images/child_ifd_jpeg.tiff", (20,)), + ), + ) + def test_get_child_images(self, path, sizes): + with Image.open(path) as im: ims = im.get_child_images() - check(ims, (16, 8)) - with Image.open("Tests/images/child_ifd_jpeg.tiff") as im: - ims = im.get_child_images() - check(ims, (20,)) + assert len(ims) == len(sizes) + for i, im in enumerate(ims): + w = sizes[i] + expected = Image.new("RGB", (w, w), "#f00") + assert_image_similar(im, expected, 1) def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] From 509dbf7757725cc644f575869d62a4a12a9f3dc4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Sep 2022 08:23:28 +1000 Subject: [PATCH 209/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 67d1500058f..1d4103a7c60 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Do not call load() before draft() in Image.thumbnail #6539 + [radarhere] + - Copy palette when converting from P to PA #6497 [radarhere] From 7d8b2fb19c596f825617f522a8cb8b869c90bf1a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 12 Sep 2022 10:25:18 +0300 Subject: [PATCH 210/242] Move some static config to setup.cfg --- setup.cfg | 4 ++++ setup.py | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index be3bc4b4f36..44feb25ff7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,11 @@ project_urls = Twitter=https://twitter.com/PythonPillow [options] +packages = PIL python_requires = >=3.7 +include_package_data = True +package_dir = + = src [options.extras_require] docs = diff --git a/setup.py b/setup.py index a2b2c691098..aa3168aa5be 100755 --- a/setup.py +++ b/setup.py @@ -999,9 +999,6 @@ def debug_build(): version=PILLOW_VERSION, cmdclass={"build_ext": pil_build_ext}, ext_modules=ext_modules, - include_package_data=True, - packages=["PIL"], - package_dir={"": "src"}, zip_safe=not (debug_build() or PLATFORM_MINGW), ) except RequiredDependencyException as err: From 8b2d70d17a4791d68df6c10d8337d769290c6528 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Sep 2022 09:10:03 +1000 Subject: [PATCH 211/242] Corrected BMP palette size when saving --- Tests/test_file_bmp.py | 12 ++++++++++++ src/PIL/BmpImagePlugin.py | 20 ++++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index d58666b4476..4c964fbeac7 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -51,6 +51,18 @@ def test_save_to_bytes(): assert reloaded.format == "BMP" +def test_small_palette(tmp_path): + im = Image.new("P", (1, 1)) + colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] + im.putpalette(colors) + + out = str(tmp_path / "temp.bmp") + im.save(out) + + with Image.open(out) as reloaded: + assert reloaded.getpalette() == colors + + def test_save_too_large(tmp_path): outfile = str(tmp_path / "temp.bmp") with Image.new("RGB", (1, 1)) as im: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 7bb73fc9388..1041ab763d2 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -375,6 +375,16 @@ def _save(im, fp, filename, bitmap_header=True): header = 40 # or 64 for OS/2 version 2 image = stride * im.size[1] + if im.mode == "1": + palette = b"".join(o8(i) * 4 for i in (0, 255)) + elif im.mode == "L": + palette = b"".join(o8(i) * 4 for i in range(256)) + elif im.mode == "P": + palette = im.im.getpalette("RGB", "BGRX") + colors = len(palette) // 4 + else: + palette = None + # bitmap header if bitmap_header: offset = 14 + header + colors * 4 @@ -405,14 +415,8 @@ def _save(im, fp, filename, bitmap_header=True): fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) - if im.mode == "1": - for i in (0, 255): - fp.write(o8(i) * 4) - elif im.mode == "L": - for i in range(256): - fp.write(o8(i) * 4) - elif im.mode == "P": - fp.write(im.im.getpalette("RGB", "BGRX")) + if palette: + fp.write(palette) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) From 964e0aa0790a7d3d9dadb03b3045de6c7e124a6e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Sep 2022 23:32:58 +1000 Subject: [PATCH 212/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1d4103a7c60..8c2993abc3b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Corrected BMP and TGA palette size when saving #6500 + [radarhere] + - Do not call load() before draft() in Image.thumbnail #6539 [radarhere] From 8b90588b9712f8105618504026646847c8814e75 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Sep 2022 16:03:23 +1000 Subject: [PATCH 213/242] Updated harfbuzz to 5.2.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 94e5dd87114..f4515468fd2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -281,9 +281,9 @@ def cmd_msbuild( "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/5.1.0.zip", - "filename": "harfbuzz-5.1.0.zip", - "dir": "harfbuzz-5.1.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/5.2.0.zip", + "filename": "harfbuzz-5.2.0.zip", + "dir": "harfbuzz-5.2.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), From 0cafaca7e8c5845272a2994f025aca201b479556 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Sep 2022 18:52:16 +1000 Subject: [PATCH 214/242] Corrected dictionary name --- docs/reference/ExifTags.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 4567d4d3e79..794fa238f6f 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -10,7 +10,7 @@ provide constants and clear-text names for various well-known EXIF tags. .. py:data:: TAGS :type: dict - The TAG dictionary maps 16-bit integer EXIF tag enumerations to + The TAGS dictionary maps 16-bit integer EXIF tag enumerations to descriptive string names. For instance: >>> from PIL.ExifTags import TAGS From d80aa74da45706ddce358130801a653830097581 Mon Sep 17 00:00:00 2001 From: Sitcebelly Date: Sun, 18 Sep 2022 21:14:52 +0300 Subject: [PATCH 215/242] Put palette into the new pad image --- src/PIL/ImageOps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 0c3f900caac..9a2dd66c9e2 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -291,6 +291,9 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 out = resized else: out = Image.new(image.mode, size, color) + palette = image.palette.copy() + if palette: + out.putpalette(palette.palette) if resized.width != size[0]: x = int((size[0] - resized.width) * max(0, min(centering[0], 1))) out.paste(resized, (x, 0)) From d88200e0d091488d13005f85bc70dfaf51ec4e03 Mon Sep 17 00:00:00 2001 From: Sitcebelly Date: Sun, 18 Sep 2022 22:32:09 +0300 Subject: [PATCH 216/242] fix bug --- src/PIL/ImageOps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 9a2dd66c9e2..b08fc69ae2e 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -293,7 +293,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 out = Image.new(image.mode, size, color) palette = image.palette.copy() if palette: - out.putpalette(palette.palette) + out.putpalette(palette) if resized.width != size[0]: x = int((size[0] - resized.width) * max(0, min(centering[0], 1))) out.paste(resized, (x, 0)) From c0aaf548161a2ef16147b2dfe1916c789cae962d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Sep 2022 12:41:20 +1000 Subject: [PATCH 217/242] Removed unnecessary palette copy --- src/PIL/ImageOps.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index b08fc69ae2e..361048d7a61 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -291,9 +291,8 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 out = resized else: out = Image.new(image.mode, size, color) - palette = image.palette.copy() - if palette: - out.putpalette(palette) + if resized.palette: + out.putpalette(resized.palette) if resized.width != size[0]: x = int((size[0] - resized.width) * max(0, min(centering[0], 1))) out.paste(resized, (x, 0)) From 1bdf6ef7203e97c8c406d074624a000f49b28a6a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 19 Sep 2022 10:36:46 +0300 Subject: [PATCH 218/242] Add OpenSSF Best Practices badge --- README.md | 3 +++ docs/index.rst | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 5e9adaf7e95..e7c0ebc5aa5 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,9 @@ As of 2019, Pillow development is Number of PyPI downloads + OpenSSF Best Practices diff --git a/docs/index.rst b/docs/index.rst index c731e274600..45af4c5714c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,6 +69,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Mon, 19 Sep 2022 21:34:29 +1000 Subject: [PATCH 219/242] Use getpalette() in ImageOps --- Tests/test_imageops.py | 14 ++++++++++++++ src/PIL/ImageOps.py | 7 +++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 01e40e6d4d5..367ba7e3e89 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -130,6 +130,20 @@ def test_pad(): ) +def test_palette(): + im = hopper("P") + + # Expand + expanded_im = ImageOps.expand(im) + assert_image_equal(im.convert("RGB"), expanded_im.convert("RGB")) + + # Pad + padded_im = ImageOps.pad(im, (256, 128), centering=(0, 0)) + assert_image_equal( + im.convert("RGB"), padded_im.convert("RGB").crop((0, 0, 128, 128)) + ) + + def test_pil163(): # Division by zero in equalize if < 255 pixels in image (@PIL163) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 361048d7a61..99f10d73982 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,7 +21,7 @@ import operator import re -from . import Image +from . import Image, ImagePalette # # helpers @@ -292,7 +292,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 else: out = Image.new(image.mode, size, color) if resized.palette: - out.putpalette(resized.palette) + out.putpalette(resized.getpalette()) if resized.width != size[0]: x = int((size[0] - resized.width) * max(0, min(centering[0], 1))) out.paste(resized, (x, 0)) @@ -399,8 +399,7 @@ def expand(image, border=0, fill=0): height = top + image.size[1] + bottom color = _color(fill, image.mode) if image.mode == "P" and image.palette: - image.load() - palette = image.palette.copy() + palette = ImagePalette.ImagePalette(palette=image.getpalette()) if isinstance(color, tuple): color = palette.getcolor(color) else: From 3c42b270b9acba34192c1c27af624d275c0f3607 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Sep 2022 21:39:38 +1000 Subject: [PATCH 220/242] Copy palette in expand() for PA --- Tests/test_imageops.py | 5 +++-- src/PIL/ImageOps.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 367ba7e3e89..1051d9843cf 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -130,8 +130,9 @@ def test_pad(): ) -def test_palette(): - im = hopper("P") +@pytest.mark.parametrize("mode", ("P", "PA")) +def test_palette(mode): + im = hopper(mode) # Expand expanded_im = ImageOps.expand(im) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 99f10d73982..57a032c9442 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -398,7 +398,7 @@ def expand(image, border=0, fill=0): width = left + image.size[0] + right height = top + image.size[1] + bottom color = _color(fill, image.mode) - if image.mode == "P" and image.palette: + if image.palette: palette = ImagePalette.ImagePalette(palette=image.getpalette()) if isinstance(color, tuple): color = palette.getcolor(color) From b12672a47af2ef04c3e4f2bfad4d8bd7987022e0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 19 Sep 2022 17:22:39 +0300 Subject: [PATCH 221/242] Fix Renovate config --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index e378ffc7877..ec3ccc8a696 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -13,5 +13,5 @@ "separateMajorMinor": "false" } ], - "schedule": ["on the third day of the month"] + "schedule": ["the 3rd day of the month"] } From 291c23f25014355953b3ad63ad85235a996ac8b3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 20 Sep 2022 07:35:39 +0300 Subject: [PATCH 222/242] Fix Renovate config Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index ec3ccc8a696..d1d82433553 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -13,5 +13,5 @@ "separateMajorMinor": "false" } ], - "schedule": ["the 3rd day of the month"] + "schedule": ["on the 3rd day of the month"] } From 25664d8201603fc841b9f74c107984bb8e4ba1b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Sep 2022 20:32:54 +1000 Subject: [PATCH 223/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8c2993abc3b..0ccb04f7852 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Use rounding in ImageOps contain() and pad() #6522 + [bibinhashley, radarhere] + +- Fixed GIF remapping to palette with duplicate entries #6548 + [radarhere] + +- Allow remap_palette() to return an image with less than 256 palette entries #6543 + [radarhere] + - Corrected BMP and TGA palette size when saving #6500 [radarhere] From 04e1b9b1218b92f978a5e14a05305d597bd5372a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Sep 2022 07:34:58 +1000 Subject: [PATCH 224/242] Updated Ghostscript to 10.0.0 --- .appveyor.yml | 4 ++-- .github/workflows/test-windows.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 1cca224ab3f..20908052bab 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -25,8 +25,8 @@ install: - mv c:\pillow-depends-main c:\pillow-depends - xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ -- ..\pillow-depends\gs9561w32.exe /S -- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH% +- ..\pillow-depends\gs1000w32.exe /S +- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index b9accfdf9a5..c04cc44bfc7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -55,8 +55,8 @@ jobs: 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH - winbuild\depends\gs9561w32.exe /S - echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH + winbuild\depends\gs1000w32.exe /S + echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH xcopy /S /Y winbuild\depends\test_images\* Tests\images\ From 652e33842b9660d95b8e3f53e8f18ad21f67151d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Sep 2022 21:45:36 +1000 Subject: [PATCH 225/242] Ensure dependencies are installed when building docs --- .github/workflows/test.yml | 1 - Makefile | 4 +++- docs/Makefile | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5614ad5f228..6abffd158e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -99,7 +99,6 @@ jobs: - name: Docs if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 run: | - python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph make doccheck - name: After success diff --git a/Makefile b/Makefile index 219dda1de50..1388a8f03a4 100644 --- a/Makefile +++ b/Makefile @@ -17,11 +17,13 @@ coverage: .PHONY: doc doc: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + python3 -c "import olefile" > /dev/null 2>&1 || python3 -m pip install olefile $(MAKE) -C docs html .PHONY: doccheck doccheck: - $(MAKE) -C docs html + $(MAKE) doc # Don't make our tests rely on the links in the docs being up every single build. # We don't control them. But do check, and update them to the target of their redirects. $(MAKE) -C docs linkcheck || true diff --git a/docs/Makefile b/docs/Makefile index f11d6b189e9..a153a2b4f8c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -44,6 +44,10 @@ clean: install-sphinx: $(PYTHON) -c "import sphinx" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx + $(PYTHON) -c "import sphinx_copybutton" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx-copybutton + $(PYTHON) -c "import sphinx_issues" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx-issues + $(PYTHON) -c "import sphinx_removed_in" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx-removed-in + $(PYTHON) -c "import sphinxext_opengraph" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinxext-opengraph $(PYTHON) -c "import furo" > /dev/null 2>&1 || $(PYTHON) -m pip install furo html: From 34f61d6d2da47894f7cb0cc571d7573ff1c9cfe4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Sep 2022 22:13:50 +1000 Subject: [PATCH 226/242] Updated redirected URLs --- docs/deprecations.rst | 2 +- docs/installation.rst | 2 +- docs/reference/ImageDraw.rst | 2 +- src/PIL/ImageFont.py | 12 ++++++------ src/libImaging/TiffDecode.c | 2 +- src/thirdparty/raqm/README.md | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 9be92770ab9..459cb47c184 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -253,7 +253,7 @@ Support for FreeType 2.7 has been removed. We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). -.. _FreeType: https://www.freetype.org +.. _FreeType: https://freetype.org/ im.offset ~~~~~~~~~ diff --git a/docs/installation.rst b/docs/installation.rst index bb547c1adaa..eb69d580567 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -184,7 +184,7 @@ Many of Pillow's features require external libraries: loads libfribidi at runtime if it is installed. On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs) - `_ + `_ (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). See `Build Options`_ to see how to build this version. * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 1ef9079fba0..64066528da1 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -731,4 +731,4 @@ Methods homogeneous, but similar, colors. .. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/ -.. _OpenType docs: https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist +.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9386d008602..310072dfc86 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -338,7 +338,7 @@ def getlength(self, text, mode="", direction=None, features=None, language=None) example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. :param language: Language of the text. Different languages may use @@ -391,7 +391,7 @@ def getbbox( example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. :param language: Language of the text. Different languages may use @@ -456,7 +456,7 @@ def getsize( example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 @@ -520,7 +520,7 @@ def getsize_multiline( example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. :param language: Language of the text. Different languages may use @@ -610,7 +610,7 @@ def getmask( example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 @@ -702,7 +702,7 @@ def getmask2( example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 04a835dcdde..7663f96a966 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -24,7 +24,7 @@ * * This cast is safe, as the top 32-bits of HFILE are guaranteed to be zero, * see - * https://docs.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication + * https://learn.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication */ #ifndef USE_WIN32_FILEIO #define fd_to_tiff_fd(fd) (fd) diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md index 02e996e7a9c..3354a4d2550 100644 --- a/src/thirdparty/raqm/README.md +++ b/src/thirdparty/raqm/README.md @@ -81,5 +81,5 @@ The following projects have patches to support complex text layout using Raqm: [1]: https://github.com/fribidi/fribidi [2]: https://github.com/Tehreer/SheenBidi [3]: https://github.com/harfbuzz/harfbuzz -[4]: https://www.freetype.org +[4]: https://freetype.org/ [5]: https://www.gtk.org/gtk-doc From de75af385ce8b2fe6a9b1c03b46c1eba2c5d8350 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Sep 2022 11:33:16 +1000 Subject: [PATCH 227/242] Replaced Codecov bash uploader with GitHub Action --- .github/workflows/test-mingw.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 7ddb71e1f68..d4bc9bde1ec 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -73,11 +73,11 @@ jobs: python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - name: Upload coverage - run: | - python3 -m pip install codecov - bash <(curl -s https://codecov.io/bash) -F GHA_Windows - env: - CODECOV_NAME: ${{ matrix.name }} + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: GHA_Windows + name: ${{ matrix.name }} success: permissions: From dd941c85c72a7805a26cc2a4bb47c2c7cfadef50 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 24 Sep 2022 19:03:32 +1000 Subject: [PATCH 228/242] Install dependencies always, but quietly Co-authored-by: Hugo van Kemenade --- docs/Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index a153a2b4f8c..7e0b43a7a7a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -43,12 +43,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -c "import sphinx" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx - $(PYTHON) -c "import sphinx_copybutton" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx-copybutton - $(PYTHON) -c "import sphinx_issues" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx-issues - $(PYTHON) -c "import sphinx_removed_in" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx-removed-in - $(PYTHON) -c "import sphinxext_opengraph" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinxext-opengraph - $(PYTHON) -c "import furo" > /dev/null 2>&1 || $(PYTHON) -m pip install furo + $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo html: $(MAKE) install-sphinx From e129ec8db7af961d764f1ae32cef0ec879c580dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Sep 2022 19:31:16 +1000 Subject: [PATCH 229/242] Moved olefile install to docs Makefile --- Makefile | 1 - docs/Makefile | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1388a8f03a4..8f2862948a8 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,6 @@ coverage: .PHONY: doc doc: python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . - python3 -c "import olefile" > /dev/null 2>&1 || python3 -m pip install olefile $(MAKE) -C docs html .PHONY: doccheck diff --git a/docs/Makefile b/docs/Makefile index 7e0b43a7a7a..458299aac7b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -43,7 +43,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo + $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile html: $(MAKE) install-sphinx From 9c8a909e833ccecd84daa5cd49b3875a0c1ad03e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Sep 2022 09:50:14 +0000 Subject: [PATCH 230/242] Update github-actions --- .github/workflows/lint.yml | 4 ++-- .github/workflows/stale.yml | 2 +- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 527f26d35d4..44f708bf826 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v3 - name: pre-commit cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pre-commit key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} @@ -24,7 +24,7 @@ jobs: lint-pre-commit- - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.10" cache: pip diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index cc5e0d488ab..620ce5eabad 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v5 + uses: actions/stale@v6 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 794159cec67..35c53aaf8f7 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -44,7 +44,7 @@ jobs: qt5-devel-tools subversion xorg-server-extra zlib-devel - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v1 + uses: egor-tensin/cleanup-path@v2 with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6062d401a88..c61dca01912 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -36,7 +36,7 @@ jobs: # sets env: pythonLocation - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} @@ -66,7 +66,7 @@ jobs: - name: Cache build id: build-cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: winbuild\build key: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7efd4651562..9a71664df2d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: pip From 7ed393cdeeb0dbb340386bfb3bd491a76d484306 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Sep 2022 19:41:30 +1000 Subject: [PATCH 231/242] Removed broken URL --- docs/releasenotes/5.2.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst index 75e8da6554d..d9b8f0fb7c8 100644 --- a/docs/releasenotes/5.2.0.rst +++ b/docs/releasenotes/5.2.0.rst @@ -105,7 +105,7 @@ Resolve confusion getting PIL / Pillow version string Re: "version constants deprecated" listed above, as user gnbl notes in #3082: - it's confusing that PIL.VERSION returns the version string of the former PIL instead of Pillow's -- there does not seem to be documentation on this version number (why this, will it ever change, ..) e.g. at https://pillow.readthedocs.io/en/5.1.x/about.html#why-a-fork +- ReadTheDocs documentation is missing for some version branches (why is this, will it ever change, ...) - it's confusing that PIL.version is a module and does not return the version information directly or hints on how to get it - the package information header is essentially useless (placeholder, does not even mention Pillow, nor the version) - PIL._version module documentation comment could explain how to access the version information From a14f9ababa78d6a87ac54f3f9496e66d3e99937a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Sep 2022 20:39:30 +1000 Subject: [PATCH 232/242] Corrected broken URLs --- docs/releasenotes/versioning.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst index 87f2ba422b3..2a0af9e59ec 100644 --- a/docs/releasenotes/versioning.rst +++ b/docs/releasenotes/versioning.rst @@ -11,7 +11,7 @@ Pillow follows `Semantic Versioning `_: 2. MINOR version when you add functionality in a backwards compatible manner, and 3. PATCH version when you make backwards compatible bug fixes. -Quarterly releases ("`Main Release `_") +Quarterly releases ("`Main Release `_") bump at least the MINOR version, as new functionality has likely been added in the prior three months. @@ -21,8 +21,8 @@ these occur every 12-18 months, guided by `Python's EOL schedule `_, and any APIs that have been deprecated for at least a year are removed at the same time. -PATCH versions ("`Point Release `_" -or "`Embargoed Release `_") +PATCH versions ("`Point Release `_" +or "`Embargoed Release `_") are for security, installation or critical bug fixes. These are less common as it is preferred to stick to quarterly releases. From a0faec1de7f1bd98989de30ce36fb5bc1ca0441d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Sep 2022 21:55:40 +1000 Subject: [PATCH 233/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0ccb04f7852..4cca8255256 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Defer parsing of palette into colors #6567 + [radarhere] + +- Apply transparency to P images in ImageTk.PhotoImage #6559 + [radarhere] + - Use rounding in ImageOps contain() and pad() #6522 [bibinhashley, radarhere] From 4c49e2dcddc0163b0f3f234429bbf72bbba07cb0 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 26 Sep 2022 07:16:33 -0500 Subject: [PATCH 234/242] add "concurrency" to GitHub actions so that old workflows are cancelled in favor of new runs --- .github/workflows/cifuzz.yml | 4 ++++ .github/workflows/lint.yml | 4 ++++ .github/workflows/release-drafter.yml | 4 ++++ .github/workflows/stale.yml | 4 ++++ .github/workflows/test-cygwin.yml | 4 ++++ .github/workflows/test-docker.yml | 4 ++++ .github/workflows/test-mingw.yml | 4 ++++ .github/workflows/test-valgrind.yml | 4 ++++ .github/workflows/test-windows.yml | 4 ++++ .github/workflows/test.yml | 4 ++++ .github/workflows/tidelift.yml | 5 +++++ 11 files changed, 45 insertions(+) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index fa1e8a50309..db030704607 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -14,6 +14,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: Fuzzing: runs-on: ubuntu-latest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 44f708bf826..6195f973b05 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 7ee76c4ac8c..9e2fdc09604 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -10,6 +10,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: update_release_draft: permissions: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 620ce5eabad..ffac91ceca7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,6 +8,10 @@ on: permissions: issues: write +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: stale: if: github.repository_owner == 'python-pillow' diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 35c53aaf8f7..5b9ab0edab3 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: windows-latest diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 5376791e970..c68d43935e2 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index d4bc9bde1ec..ccf6e193a6d 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: windows-latest diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index dda1b357785..219189cf208 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -16,6 +16,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c61dca01912..36bd03e7ee9 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: windows-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index acec2e81bee..4c8a1b85f74 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml index c73f254313d..69f9e547602 100644 --- a/.github/workflows/tidelift.yml +++ b/.github/workflows/tidelift.yml @@ -1,4 +1,5 @@ name: Tidelift Align + on: schedule: - cron: "30 2 * * *" # daily at 02:30 UTC @@ -15,6 +16,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: if: github.repository_owner == 'python-pillow' From a1299695c1983be02196c15414a97417ab6bccea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Sep 2022 07:29:03 +1000 Subject: [PATCH 235/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4cca8255256..fb3879d7970 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Improved ImageOps palette handling #6596 + [PososikTeam, radarhere] + - Defer parsing of palette into colors #6567 [radarhere] From 64d11d9123fe3de62a1ed50247635bb2743b9197 Mon Sep 17 00:00:00 2001 From: David Walker Date: Wed, 28 Sep 2022 20:08:49 -0700 Subject: [PATCH 236/242] Improve documentation for ImageDraw.rectangle and rounded_rectangle Fixes #1668 --- docs/reference/ImageDraw.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 64066528da1..d102056a5ed 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -285,8 +285,8 @@ Methods Draws a rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point - is just outside the drawn rectangle. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box + is inclusive of both endpoints. :param outline: Color to use for the outline. :param fill: Color to use for the fill. :param width: The line width, in pixels. @@ -298,8 +298,8 @@ Methods Draws a rounded rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point - is just outside the drawn rectangle. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box + is inclusive of both endpoints. :param radius: Radius of the corners. :param outline: Color to use for the outline. :param fill: Color to use for the fill. From 15b2b7a9deff44e8c3bb31f65a726dc70e65f398 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Oct 2022 19:54:35 +1000 Subject: [PATCH 237/242] Added headings before listing options --- docs/handbook/image-file-formats.rst | 109 +++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ff54853a337..dc629666c30 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -31,6 +31,9 @@ BLP is the Blizzard Mipmap Format, a texture format used in World of Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1`` images, and all types of ``BLP2`` images. +Saving +~~~~~~ + Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: @@ -46,6 +49,9 @@ or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length enc is not supported. Support for reading 8-bit run-length encoding was added in Pillow 9.1.0. +Opening +~~~~~~~ + The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -78,6 +84,9 @@ EPS images. The EPS driver can read EPS images in ``L``, ``LAB``, ``RGB`` and than leaving them in the original color space. The EPS driver can write images in ``L``, ``RGB`` and ``CMYK`` modes. +Loading +~~~~~~~ + If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load` method with the following parameters to affect how Ghostscript renders the EPS @@ -134,6 +143,11 @@ To restore the default behavior, where ``P`` mode images are only converted to from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST +.. _gif-opening: + +Opening +~~~~~~~ + The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -171,6 +185,8 @@ to seek to the next frame (``im.seek(im.tell() + 1)``). ``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame. +.. _gif-saving: + Saving ~~~~~~ @@ -278,6 +294,11 @@ sets the following :py:attr:`~PIL.Image.Image.info` property: ask for ``(512, 512, 2)``, the final value of :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). +.. _icns-saving: + +Saving +~~~~~~ + The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: **append_images** @@ -292,6 +313,11 @@ ICO ICO is used to store icons on Windows. The largest available icon is read. +.. _ico-saving: + +Saving +~~~~~~ + The :py:meth:`~PIL.Image.Image.save` method supports the following options: **sizes** @@ -337,6 +363,11 @@ their original size while loading them. By default Pillow doesn't allow loading of truncated JPEG files, set :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this. +.. _jpeg-opening: + +Opening +~~~~~~~ + The :py:meth:`~PIL.Image.open` method may set the following :py:attr:`~PIL.Image.Image.info` properties if available: @@ -383,6 +414,10 @@ The :py:meth:`~PIL.Image.open` method may set the following .. versionadded:: 7.1.0 +.. _jpeg-saving: + +Saving +~~~~~~ The :py:meth:`~PIL.Image.Image.save` method supports the following options: @@ -464,6 +499,11 @@ itself. It is also possible to set ``reduce`` to the number of resolutions to discard (each one reduces the size of the resulting image by a factor of 2), and ``layers`` to specify the number of quality layers to load. +.. _jpeg-2000-saving: + +Saving +~~~~~~ + The :py:meth:`~PIL.Image.Image.save` method supports the following options: **offset** @@ -575,6 +615,11 @@ called. By default Pillow doesn't allow loading of truncated PNG files, set :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this. +.. _png-opening: + +Opening +~~~~~~~ + The :py:func:`~PIL.Image.open` function sets the following :py:attr:`~PIL.Image.Image.info` properties, when appropriate: @@ -613,6 +658,11 @@ decompression bombs. Additionally, the total size of all of the text chunks is limited to :data:`.PngImagePlugin.MAX_TEXT_MEMORY`, defaulting to 64MB. +.. _png-saving: + +Saving +~~~~~~ + The :py:meth:`~PIL.Image.Image.save` method supports the following options: **optimize** @@ -803,6 +853,11 @@ Pillow also reads SPIDER stack files containing sequences of SPIDER images. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and random access is allowed. +.. _spider-opening: + +Opening +~~~~~~~ + The :py:meth:`~PIL.Image.open` method sets the following attributes: **format** @@ -819,8 +874,10 @@ is provided for converting floating point data to byte data (mode ``L``):: im = Image.open("image001.spi").convert2byte() -Writing files in SPIDER format -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _spider-saving: + +Saving +~~~~~~ The extension of SPIDER files may be any 3 alphanumeric characters. Therefore the output format must be specified explicitly:: @@ -837,6 +894,11 @@ Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``, ``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and run-length encoded TGAs. +.. _tga-saving: + +Saving +~~~~~~ + The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: **compression** @@ -871,6 +933,11 @@ uncompressed files. support for reading Packbits, LZW and JPEG compressed TIFFs without using libtiff. +.. _tiff-opening: + +Opening +~~~~~~~ + The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -922,8 +989,10 @@ and can be accessed in any order. ``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame. -Saving Tiff Images -~~~~~~~~~~~~~~~~~~ +.. _tiff-saving: + +Saving +~~~~~~ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: @@ -1035,6 +1104,11 @@ WebP Pillow reads and writes WebP files. The specifics of Pillow's capabilities with this format are currently undocumented. +.. _webp-saving: + +Saving +~~~~~~ + The :py:meth:`~PIL.Image.Image.save` method supports the following options: **lossless** @@ -1058,7 +1132,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: the system WebP library was built with webpmux support. Saving sequences -~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~ .. note:: @@ -1173,6 +1247,11 @@ GBR The GBR decoder reads GIMP brush files, version 1 and 2. +.. _gbr-opening: + +Opening +~~~~~~~ + The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -1188,6 +1267,11 @@ GD Pillow reads uncompressed GD2 files. Note that you must use :py:func:`PIL.GdImageFile.open` to read such a file. +.. _gd-opening: + +Opening +~~~~~~~ + The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -1227,6 +1311,11 @@ image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL methods may be used to read other pictures from the file. The pictures are zero-indexed and random access is supported. +.. _mpo-saving: + +Saving +~~~~~~ + When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default only the first frame of a multiframe image will be saved. If the ``save_all`` argument is present and true, then all frames will be saved, and the following @@ -1326,6 +1415,11 @@ XPM Pillow reads X pixmap files (mode ``P``) with 256 colors or less. +.. _xpm-opening: + +Opening +~~~~~~~ + The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -1350,6 +1444,11 @@ Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4 files, using either JPEG or HEX encoding depending on the image mode (and whether JPEG support is available or not). +.. _pdf-saving: + +Saving +~~~~~~ + The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: **save_all** From 2a296be9861cb36096d236ab4a6c6c9964bffb79 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Oct 2022 13:37:56 +1100 Subject: [PATCH 238/242] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fb3879d7970..c3e60acff7e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.3.0 (unreleased) ------------------ +- Added reading of TIFF child images #6569 + [radarhere] + - Improved ImageOps palette handling #6596 [PososikTeam, radarhere] From 985fec2f563b719c2c8818014756ee23416d3644 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Oct 2022 16:15:29 +1100 Subject: [PATCH 239/242] Removed duplicate test --- Tests/test_file_eps.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 766c5064920..cbb89aeda80 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -124,14 +124,6 @@ def test_file_object(tmp_path): image1.save(fh, "EPS") -@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_iobase_object(tmp_path): - # issue 479 - with Image.open(FILE1) as image1: - with open(str(tmp_path / "temp_iobase.eps"), "wb") as fh: - image1.save(fh, "EPS") - - @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_bytesio_object(): with open(FILE1, "rb") as f: From c259ac492fd69571c658cc3b094605d22c202f13 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Oct 2022 16:57:42 +1100 Subject: [PATCH 240/242] Parametrized tests --- Tests/test_features.py | 12 ++-- Tests/test_file_apng.py | 13 ++-- Tests/test_file_eps.py | 45 +++++++------- Tests/test_file_gif.py | 34 +++++------ Tests/test_file_jpeg.py | 61 ++++++++++--------- Tests/test_file_jpeg2k.py | 29 ++++----- Tests/test_file_libtiff.py | 9 +-- Tests/test_file_palm.py | 16 +---- Tests/test_file_pcx.py | 10 +-- Tests/test_file_pdf.py | 37 ++---------- Tests/test_file_tiff.py | 48 +++++++-------- Tests/test_font_pcf_charsets.py | 47 +++------------ Tests/test_image_reduce.py | 8 +-- Tests/test_image_resample.py | 74 ++++++++++++----------- Tests/test_image_split.py | 21 +++---- Tests/test_imagedraw.py | 104 +++++++------------------------- Tests/test_imagedraw2.py | 47 +++------------ Tests/test_imageenhance.py | 20 +++--- Tests/test_imagefont.py | 34 +++++------ Tests/test_imagemorph.py | 16 ++--- Tests/test_imageshow.py | 40 ++++++------ Tests/test_imagetk.py | 36 +++++------ Tests/test_mode_i16.py | 73 ++++++++++------------ Tests/test_numpy.py | 30 +++++---- Tests/test_pickle.py | 8 +-- 25 files changed, 350 insertions(+), 522 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 284f72205f3..c4e9cd36811 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -70,14 +70,14 @@ def test_libimagequant_version(): assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) -def test_check_modules(): - for feature in features.modules: - assert features.check_module(feature) in [True, False] +@pytest.mark.parametrize("feature", features.modules) +def test_check_modules(feature): + assert features.check_module(feature) in [True, False] -def test_check_codecs(): - for feature in features.codecs: - assert features.check_codec(feature) in [True, False] +@pytest.mark.parametrize("feature", features.codecs) +def test_check_codecs(feature): + assert features.check_codec(feature) in [True, False] def test_check_warns_on_nonexistent(): diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 0ff05f608c2..cdaad5940af 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -39,13 +39,12 @@ def test_apng_basic(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_fdat(): - with Image.open("Tests/images/apng/split_fdat.png") as im: - im.seek(im.n_frames - 1) - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) - - with Image.open("Tests/images/apng/split_fdat_zero_chunk.png") as im: +@pytest.mark.parametrize( + "filename", + ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), +) +def test_apng_fdat(filename): + with Image.open(filename) as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index cbb89aeda80..015dda992c6 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -195,25 +195,23 @@ def test_render_scale2(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_resize(): - files = [FILE1, FILE2, "Tests/images/illu10_preview.eps"] - for fn in files: - with Image.open(fn) as im: - new_size = (100, 100) - im = im.resize(new_size) - assert im.size == new_size +@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) +def test_resize(filename): + with Image.open(filename) as im: + new_size = (100, 100) + im = im.resize(new_size) + assert im.size == new_size @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_thumbnail(): +@pytest.mark.parametrize("filename", (FILE1, FILE2)) +def test_thumbnail(filename): # Issue #619 # Arrange - files = [FILE1, FILE2] - for fn in files: - with Image.open(FILE1) as im: - new_size = (100, 100) - im.thumbnail(new_size) - assert max(im.size) == max(new_size) + with Image.open(filename) as im: + new_size = (100, 100) + im.thumbnail(new_size) + assert max(im.size) == max(new_size) def test_read_binary_preview(): @@ -258,20 +256,19 @@ def _test_readline_file_psfile(test_string, ending): _test_readline_file_psfile(s, ending) -def test_open_eps(): - # https://github.com/python-pillow/Pillow/issues/1104 - # Arrange - FILES = [ +@pytest.mark.parametrize( + "filename", + ( "Tests/images/illu10_no_preview.eps", "Tests/images/illu10_preview.eps", "Tests/images/illuCS6_no_preview.eps", "Tests/images/illuCS6_preview.eps", - ] - - # Act / Assert - for filename in FILES: - with Image.open(filename) as img: - assert img.mode == "RGB" + ), +) +def test_open_eps(filename): + # https://github.com/python-pillow/Pillow/issues/1104 + with Image.open(filename) as img: + assert img.mode == "RGB" @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 4e967faec91..637be2be485 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -793,24 +793,24 @@ def test_identical_frames(tmp_path): assert reread.info["duration"] == 4500 -def test_identical_frames_to_single_frame(tmp_path): - for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500): - out = str(tmp_path / "temp.gif") - im_list = [ - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - Image.new("L", (100, 100), "#000"), - ] - - im_list[0].save( - out, save_all=True, append_images=im_list[1:], duration=duration - ) - with Image.open(out) as reread: - # Assert that all frames were combined - assert reread.n_frames == 1 +@pytest.mark.parametrize( + "duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500) +) +def test_identical_frames_to_single_frame(duration, tmp_path): + out = str(tmp_path / "temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + ] - # Assert that the new duration is the total of the identical frames - assert reread.info["duration"] == 8500 + im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) + with Image.open(out) as reread: + # Assert that all frames were combined + assert reread.n_frames == 1 + + # Assert that the new duration is the total of the identical frames + assert reread.info["duration"] == 8500 def test_number_of_loops(tmp_path): diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 12edd75828e..adbb72aa576 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -150,27 +150,30 @@ def test_icc(self, tmp_path): assert not im1.info.get("icc_profile") assert im2.info.get("icc_profile") - def test_icc_big(self): + @pytest.mark.parametrize( + "n", + ( + 0, + 1, + 3, + 4, + 5, + 65533 - 14, # full JPEG marker block + 65533 - 14 + 1, # full block plus one byte + ImageFile.MAXBLOCK, # full buffer block + ImageFile.MAXBLOCK + 1, # full buffer block plus one byte + ImageFile.MAXBLOCK * 4 + 3, # large block + ), + ) + def test_icc_big(self, n): # Make sure that the "extra" support handles large blocks - def test(n): - # The ICC APP marker can store 65519 bytes per marker, so - # using a 4-byte test code should allow us to detect out of - # order issues. - icc_profile = (b"Test" * int(n / 4 + 1))[:n] - assert len(icc_profile) == n # sanity - im1 = self.roundtrip(hopper(), icc_profile=icc_profile) - assert im1.info.get("icc_profile") == (icc_profile or None) - - test(0) - test(1) - test(3) - test(4) - test(5) - test(65533 - 14) # full JPEG marker block - test(65533 - 14 + 1) # full block plus one byte - test(ImageFile.MAXBLOCK) # full buffer block - test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte - test(ImageFile.MAXBLOCK * 4 + 3) # large block + # The ICC APP marker can store 65519 bytes per marker, so + # using a 4-byte test code should allow us to detect out of + # order issues. + icc_profile = (b"Test" * int(n / 4 + 1))[:n] + assert len(icc_profile) == n # sanity + im1 = self.roundtrip(hopper(), icc_profile=icc_profile) + assert im1.info.get("icc_profile") == (icc_profile or None) @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" @@ -649,19 +652,19 @@ def test_bad_mpo_header(self): # Assert assert im.format == "JPEG" - def test_save_correct_modes(self): + @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) + def test_save_correct_modes(self, mode): out = BytesIO() - for mode in ["1", "L", "RGB", "RGBX", "CMYK", "YCbCr"]: - img = Image.new(mode, (20, 20)) - img.save(out, "JPEG") + img = Image.new(mode, (20, 20)) + img.save(out, "JPEG") - def test_save_wrong_modes(self): + @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) + def test_save_wrong_modes(self, mode): # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() - for mode in ["LA", "La", "RGBA", "RGBa", "P"]: - img = Image.new(mode, (20, 20)) - with pytest.raises(OSError): - img.save(out, "JPEG") + img = Image.new(mode, (20, 20)) + with pytest.raises(OSError): + img.save(out, "JPEG") def test_save_tiff_with_dpi(self, tmp_path): # Arrange diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 7942d6b9afd..cd142e67fc7 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -126,14 +126,14 @@ def test_prog_res_rt(): assert_image_equal(im, test_card) -def test_default_num_resolutions(): - for num_resolutions in range(2, 6): - d = 1 << (num_resolutions - 1) - im = test_card.resize((d - 1, d - 1)) - with pytest.raises(OSError): - roundtrip(im, num_resolutions=num_resolutions) - reloaded = roundtrip(im) - assert_image_equal(im, reloaded) +@pytest.mark.parametrize("num_resolutions", range(2, 6)) +def test_default_num_resolutions(num_resolutions): + d = 1 << (num_resolutions - 1) + im = test_card.resize((d - 1, d - 1)) + with pytest.raises(OSError): + roundtrip(im, num_resolutions=num_resolutions) + reloaded = roundtrip(im) + assert_image_equal(im, reloaded) def test_reduce(): @@ -266,14 +266,11 @@ def test_rgba(): assert jp2.mode == "RGBA" -def test_16bit_monochrome_has_correct_mode(): - with Image.open("Tests/images/16bit.cropped.j2k") as j2k: - j2k.load() - assert j2k.mode == "I;16" - - with Image.open("Tests/images/16bit.cropped.jp2") as jp2: - jp2.load() - assert jp2.mode == "I;16" +@pytest.mark.parametrize("ext", (".j2k", ".jp2")) +def test_16bit_monochrome_has_correct_mode(ext): + with Image.open("Tests/images/16bit.cropped" + ext) as im: + im.load() + assert im.mode == "I;16" def test_16bit_monochrome_jp2_like_tiff(): diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 86a0fda04e5..6015a8fec22 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -509,7 +509,8 @@ def test_palette_save(self, im, tmp_path): # colormap/palette tag assert len(reloaded.tag_v2[320]) == 768 - def xtest_bw_compression_w_rgb(self, tmp_path): + @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) + def xtest_bw_compression_w_rgb(self, compression, tmp_path): """This test passes, but when running all tests causes a failure due to output on stderr from the error thrown by libtiff. We need to capture that but not now""" @@ -518,11 +519,7 @@ def xtest_bw_compression_w_rgb(self, tmp_path): out = str(tmp_path / "temp.tif") with pytest.raises(OSError): - im.save(out, compression="tiff_ccitt") - with pytest.raises(OSError): - im.save(out, compression="group3") - with pytest.raises(OSError): - im.save(out, compression="group4") + im.save(out, compression=compression) def test_fp_leak(self): im = Image.open("Tests/images/hopper_g4_500.tif") diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index e1c1c361b1e..be7c8d0c86a 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -63,19 +63,7 @@ def test_p_mode(tmp_path): roundtrip(tmp_path, mode) -def test_l_oserror(tmp_path): - # Arrange - mode = "L" - - # Act / Assert - with pytest.raises(OSError): - helper_save_as_palm(tmp_path, mode) - - -def test_rgb_oserror(tmp_path): - # Arrange - mode = "RGB" - - # Act / Assert +@pytest.mark.parametrize("mode", ("L", "RGB")) +def test_oserror(tmp_path, mode): with pytest.raises(OSError): helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index ba6663cd3ba..485adf7853e 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -39,14 +39,14 @@ def test_invalid_file(): PcxImagePlugin.PcxImageFile(invalid_file) -def test_odd(tmp_path): +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) +def test_odd(tmp_path, mode): # See issue #523, odd sized images should have a stride that's even. # Not that ImageMagick or GIMP write PCX that way. # We were not handling properly. - for mode in ("1", "L", "P", "RGB"): - # larger, odd sized images are better here to ensure that - # we handle interrupted scan lines properly. - _roundtrip(tmp_path, hopper(mode).resize((511, 511))) + # larger, odd sized images are better here to ensure that + # we handle interrupted scan lines properly. + _roundtrip(tmp_path, hopper(mode).resize((511, 511))) def test_odd_read(): diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index b5273353ce3..4129e878317 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -37,6 +37,11 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): return outfile +@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) +def test_save(tmp_path, mode): + helper_save_as_pdf(tmp_path, mode) + + @pytest.mark.valgrind_known_error(reason="Temporary skip") def test_monochrome(tmp_path): # Arrange @@ -47,38 +52,6 @@ def test_monochrome(tmp_path): assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) -def test_greyscale(tmp_path): - # Arrange - mode = "L" - - # Act / Assert - helper_save_as_pdf(tmp_path, mode) - - -def test_rgb(tmp_path): - # Arrange - mode = "RGB" - - # Act / Assert - helper_save_as_pdf(tmp_path, mode) - - -def test_p_mode(tmp_path): - # Arrange - mode = "P" - - # Act / Assert - helper_save_as_pdf(tmp_path, mode) - - -def test_cmyk_mode(tmp_path): - # Arrange - mode = "CMYK" - - # Act / Assert - helper_save_as_pdf(tmp_path, mode) - - def test_unsupported_mode(tmp_path): im = hopper("LA") outfile = str(tmp_path / "temp_LA.pdf") diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index ac0bd7f6096..1a5ba594f3f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -311,14 +311,17 @@ def test_unknown_pixel_mode(self): with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): pass - def test_n_frames(self): - for path, n_frames in [ - ["Tests/images/multipage-lastframe.tif", 1], - ["Tests/images/multipage.tiff", 3], - ]: - with Image.open(path) as im: - assert im.n_frames == n_frames - assert im.is_animated == (n_frames != 1) + @pytest.mark.parametrize( + "path, n_frames", + ( + ("Tests/images/multipage-lastframe.tif", 1), + ("Tests/images/multipage.tiff", 3), + ), + ) + def test_n_frames(self, path, n_frames): + with Image.open(path) as im: + assert im.n_frames == n_frames + assert im.is_animated == (n_frames != 1) def test_eoferror(self): with Image.open("Tests/images/multipage-lastframe.tif") as im: @@ -434,12 +437,12 @@ def test__delitem__(self): len_after = len(dict(im.ifd)) assert len_before == len_after + 1 - def test_load_byte(self): - for legacy_api in [False, True]: - ifd = TiffImagePlugin.ImageFileDirectory_v2() - data = b"abc" - ret = ifd.load_byte(data, legacy_api) - assert ret == b"abc" + @pytest.mark.parametrize("legacy_api", (False, True)) + def test_load_byte(self, legacy_api): + ifd = TiffImagePlugin.ImageFileDirectory_v2() + data = b"abc" + ret = ifd.load_byte(data, legacy_api) + assert ret == b"abc" def test_load_string(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() @@ -685,18 +688,15 @@ def test_planar_configuration_save(self, tmp_path): with Image.open(outfile) as reloaded: assert_image_equal_tofile(reloaded, infile) - def test_palette(self, tmp_path): - def roundtrip(mode): - outfile = str(tmp_path / "temp.tif") - - im = hopper(mode) - im.save(outfile) + @pytest.mark.parametrize("mode", ("P", "PA")) + def test_palette(self, mode, tmp_path): + outfile = str(tmp_path / "temp.tif") - with Image.open(outfile) as reloaded: - assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) + im = hopper(mode) + im.save(outfile) - for mode in ["P", "PA"]: - roundtrip(mode) + with Image.open(outfile) as reloaded: + assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) def test_tiff_save_all(self): mp = BytesIO() diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 4477ee29d55..664663fd6bb 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,5 +1,7 @@ import os +import pytest + from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile from .helper import ( @@ -59,23 +61,13 @@ def delete_tempfile(): return tempname -def _test_sanity(request, tmp_path, encoding): +@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) +def test_sanity(request, tmp_path, encoding): save_font(request, tmp_path, encoding) -def test_sanity_iso8859_1(request, tmp_path): - _test_sanity(request, tmp_path, "iso8859-1") - - -def test_sanity_iso8859_2(request, tmp_path): - _test_sanity(request, tmp_path, "iso8859-2") - - -def test_sanity_cp1250(request, tmp_path): - _test_sanity(request, tmp_path, "cp1250") - - -def _test_draw(request, tmp_path, encoding): +@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) +def test_draw(request, tmp_path, encoding): tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") @@ -85,19 +77,8 @@ def _test_draw(request, tmp_path, encoding): assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) -def test_draw_iso8859_1(request, tmp_path): - _test_draw(request, tmp_path, "iso8859-1") - - -def test_draw_iso8859_2(request, tmp_path): - _test_draw(request, tmp_path, "iso8859-2") - - -def test_draw_cp1250(request, tmp_path): - _test_draw(request, tmp_path, "cp1250") - - -def _test_textsize(request, tmp_path, encoding): +@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) +def test_textsize(request, tmp_path, encoding): tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) for i in range(255): @@ -112,15 +93,3 @@ def _test_textsize(request, tmp_path, encoding): msg = message[: i + 1] assert font.getlength(msg) == len(msg) * 10 assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) - - -def test_textsize_iso8859_1(request, tmp_path): - _test_textsize(request, tmp_path, "iso8859-1") - - -def test_textsize_iso8859_2(request, tmp_path): - _test_textsize(request, tmp_path, "iso8859-2") - - -def test_textsize_cp1250(request, tmp_path): - _test_textsize(request, tmp_path, "cp1250") diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index 801161511b9..ae8d740a027 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -196,11 +196,11 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255): ) -def test_mode_L(): +@pytest.mark.parametrize("factor", remarkable_factors) +def test_mode_L(factor): im = get_image("L") - for factor in remarkable_factors: - compare_reduce_with_reference(im, factor) - compare_reduce_with_box(im, factor) + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 5ce98a23568..53ceb6df030 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -554,44 +554,48 @@ def test_no_passthrough(self): # check that the difference at least that much assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}") - def test_skip_horizontal(self): + @pytest.mark.parametrize( + "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) + ) + def test_skip_horizontal(self, flt): # Can skip resize for one dimension im = hopper() - for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]: - for size, box in [ - ((40, 50), (0, 0, 40, 90)), - ((40, 50), (0, 20, 40, 90)), - ((40, 50), (10, 0, 50, 90)), - ((40, 50), (10, 20, 50, 90)), - ]: - res = im.resize(size, flt, box) - assert res.size == size - # Borders should be slightly different - assert_image_similar( - res, - im.crop(box).resize(size, flt), - 0.4, - f">>> {size} {box} {flt}", - ) - - def test_skip_vertical(self): + for size, box in [ + ((40, 50), (0, 0, 40, 90)), + ((40, 50), (0, 20, 40, 90)), + ((40, 50), (10, 0, 50, 90)), + ((40, 50), (10, 20, 50, 90)), + ]: + res = im.resize(size, flt, box) + assert res.size == size + # Borders should be slightly different + assert_image_similar( + res, + im.crop(box).resize(size, flt), + 0.4, + f">>> {size} {box} {flt}", + ) + + @pytest.mark.parametrize( + "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) + ) + def test_skip_vertical(self, flt): # Can skip resize for one dimension im = hopper() - for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]: - for size, box in [ - ((40, 50), (0, 0, 90, 50)), - ((40, 50), (20, 0, 90, 50)), - ((40, 50), (0, 10, 90, 60)), - ((40, 50), (20, 10, 90, 60)), - ]: - res = im.resize(size, flt, box) - assert res.size == size - # Borders should be slightly different - assert_image_similar( - res, - im.crop(box).resize(size, flt), - 0.4, - f">>> {size} {box} {flt}", - ) + for size, box in [ + ((40, 50), (0, 0, 90, 50)), + ((40, 50), (20, 0, 90, 50)), + ((40, 50), (0, 10, 90, 60)), + ((40, 50), (20, 10, 90, 60)), + ]: + res = im.resize(size, flt, box) + assert res.size == size + # Borders should be slightly different + assert_image_similar( + res, + im.crop(box).resize(size, flt), + 0.4, + f">>> {size} {box} {flt}", + ) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index fbed276b8b7..5cb7c9a8be8 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image, features from .helper import assert_image_equal, hopper @@ -29,19 +31,12 @@ def split(mode): assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] -def test_split_merge(): - def split_merge(mode): - return Image.merge(mode, hopper(mode).split()) - - assert_image_equal(hopper("1"), split_merge("1")) - assert_image_equal(hopper("L"), split_merge("L")) - assert_image_equal(hopper("I"), split_merge("I")) - assert_image_equal(hopper("F"), split_merge("F")) - assert_image_equal(hopper("P"), split_merge("P")) - assert_image_equal(hopper("RGB"), split_merge("RGB")) - assert_image_equal(hopper("RGBA"), split_merge("RGBA")) - assert_image_equal(hopper("CMYK"), split_merge("CMYK")) - assert_image_equal(hopper("YCbCr"), split_merge("YCbCr")) +@pytest.mark.parametrize( + "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr") +) +def test_split_merge(mode): + expected = Image.merge(mode, hopper(mode).split()) + assert_image_equal(hopper(mode), expected) def test_split_open(tmp_path): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index d1dd1e47c1c..76b7c65cc37 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -64,7 +64,9 @@ def test_mode_mismatch(): ImageDraw.ImageDraw(im, mode="L") -def helper_arc(bbox, start, end): +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) +def test_arc(bbox, start, end): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -76,16 +78,6 @@ def helper_arc(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) -def test_arc1(): - helper_arc(BBOX1, 0, 180) - helper_arc(BBOX1, 0.5, 180.4) - - -def test_arc2(): - helper_arc(BBOX2, 0, 180) - helper_arc(BBOX2, 0.5, 180.4) - - def test_arc_end_le_start(): # Arrange im = Image.new("RGB", (W, H)) @@ -192,29 +184,21 @@ def test_bitmap(): assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png") -def helper_chord(mode, bbox, start, end): +@pytest.mark.parametrize("mode", ("RGB", "L")) +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_chord(mode, bbox): # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) expected = f"Tests/images/imagedraw_chord_{mode}.png" # Act - draw.chord(bbox, start, end, fill="red", outline="yellow") + draw.chord(bbox, 0, 180, fill="red", outline="yellow") # Assert assert_image_similar_tofile(im, expected, 1) -def test_chord1(): - for mode in ["RGB", "L"]: - helper_chord(mode, BBOX1, 0, 180) - - -def test_chord2(): - for mode in ["RGB", "L"]: - helper_chord(mode, BBOX2, 0, 180) - - def test_chord_width(): # Arrange im = Image.new("RGB", (W, H)) @@ -263,7 +247,9 @@ def test_chord_too_fat(): assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png") -def helper_ellipse(mode, bbox): +@pytest.mark.parametrize("mode", ("RGB", "L")) +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_ellipse(mode, bbox): # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -276,16 +262,6 @@ def helper_ellipse(mode, bbox): assert_image_similar_tofile(im, expected, 1) -def test_ellipse1(): - for mode in ["RGB", "L"]: - helper_ellipse(mode, BBOX1) - - -def test_ellipse2(): - for mode in ["RGB", "L"]: - helper_ellipse(mode, BBOX2) - - def test_ellipse_translucent(): # Arrange im = Image.new("RGB", (W, H)) @@ -405,7 +381,8 @@ def test_ellipse_various_sizes_filled(): ) -def helper_line(points): +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_line(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -417,14 +394,6 @@ def helper_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_line1(): - helper_line(POINTS1) - - -def test_line2(): - helper_line(POINTS2) - - def test_shape1(): # Arrange im = Image.new("RGB", (100, 100), "white") @@ -484,7 +453,9 @@ def test_transform(): assert_image_equal(im, expected) -def helper_pieslice(bbox, start, end): +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) +def test_pieslice(bbox, start, end): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -496,16 +467,6 @@ def helper_pieslice(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) -def test_pieslice1(): - helper_pieslice(BBOX1, -92, 46) - helper_pieslice(BBOX1, -92.2, 46.2) - - -def test_pieslice2(): - helper_pieslice(BBOX2, -92, 46) - helper_pieslice(BBOX2, -92.2, 46.2) - - def test_pieslice_width(): # Arrange im = Image.new("RGB", (W, H)) @@ -585,7 +546,8 @@ def test_pieslice_no_spikes(): assert_image_equal(im, im_pre_erase) -def helper_point(points): +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_point(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -597,15 +559,8 @@ def helper_point(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -def test_point1(): - helper_point(POINTS1) - - -def test_point2(): - helper_point(POINTS2) - - -def helper_polygon(points): +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -617,14 +572,6 @@ def helper_polygon(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") -def test_polygon1(): - helper_polygon(POINTS1) - - -def test_polygon2(): - helper_polygon(POINTS2) - - @pytest.mark.parametrize("mode", ("RGB", "L")) def test_polygon_kite(mode): # Test drawing lines of different gradients (dx>dy, dy>dx) and @@ -682,7 +629,8 @@ def test_polygon_translucent(): assert_image_equal_tofile(im, expected) -def helper_rectangle(bbox): +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -694,14 +642,6 @@ def helper_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_rectangle1(): - helper_rectangle(BBOX1) - - -def test_rectangle2(): - helper_rectangle(BBOX2) - - def test_big_rectangle(): # Test drawing a rectangle bigger than the image # Arrange @@ -1503,7 +1443,7 @@ def test_discontiguous_corners_polygon(): assert_image_similar_tofile(img, expected, 1) -def test_polygon(): +def test_polygon2(): im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index e4e8a38cb59..6fc829f1a54 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -52,27 +52,19 @@ def test_sanity(): draw.line(list(range(10)), pen) -def helper_ellipse(mode, bbox): +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_ellipse(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) pen = ImageDraw2.Pen("blue", width=2) brush = ImageDraw2.Brush("green") - expected = f"Tests/images/imagedraw_ellipse_{mode}.png" # Act draw.ellipse(bbox, pen, brush) # Assert - assert_image_similar_tofile(im, expected, 1) - - -def test_ellipse1(): - helper_ellipse("RGB", BBOX1) - - -def test_ellipse2(): - helper_ellipse("RGB", BBOX2) + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1) def test_ellipse_edge(): @@ -88,7 +80,8 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -def helper_line(points): +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_line(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -101,14 +94,6 @@ def helper_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_line1_pen(): - helper_line(POINTS1) - - -def test_line2_pen(): - helper_line(POINTS2) - - def test_line_pen_as_brush(): # Arrange im = Image.new("RGB", (W, H)) @@ -124,7 +109,8 @@ def test_line_pen_as_brush(): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def helper_polygon(points): +@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -138,15 +124,8 @@ def helper_polygon(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") -def test_polygon1(): - helper_polygon(POINTS1) - - -def test_polygon2(): - helper_polygon(POINTS2) - - -def helper_rectangle(bbox): +@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -160,14 +139,6 @@ def helper_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_rectangle1(): - helper_rectangle(BBOX1) - - -def test_rectangle2(): - helper_rectangle(BBOX2) - - def test_big_rectangle(): # Test drawing a rectangle bigger than the image # Arrange diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index 8bc94401e80..221ef8cdb26 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image, ImageEnhance from .helper import assert_image_equal, hopper @@ -39,17 +41,17 @@ def _check_alpha(im, original, op, amount): ) -def test_alpha(): +@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) +def test_alpha(op): # Issue https://github.com/python-pillow/Pillow/issues/899 # Is alpha preserved through image enhancement? original = _half_transparent_image() - for op in ["Color", "Brightness", "Contrast", "Sharpness"]: - for amount in [0, 0.5, 1.0]: - _check_alpha( - getattr(ImageEnhance, op)(original).enhance(amount), - original, - op, - amount, - ) + for amount in [0, 0.5, 1.0]: + _check_alpha( + getattr(ImageEnhance, op)(original).enhance(amount), + original, + op, + amount, + ) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 09e5370e2d4..a374e24c5bb 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -632,24 +632,24 @@ def test_imagefont_getters(font): assert len(log) == 11 -def test_getsize_stroke(font): - for stroke_width in [0, 2]: - assert font.getbbox("A", stroke_width=stroke_width) == ( - 0 - stroke_width, - 4 - stroke_width, - 12 + stroke_width, - 16 + stroke_width, +@pytest.mark.parametrize("stroke_width", (0, 2)) +def test_getsize_stroke(font, stroke_width): + assert font.getbbox("A", stroke_width=stroke_width) == ( + 0 - stroke_width, + 4 - stroke_width, + 12 + stroke_width, + 16 + stroke_width, + ) + with pytest.warns(DeprecationWarning) as log: + assert font.getsize("A", stroke_width=stroke_width) == ( + 12 + stroke_width * 2, + 16 + stroke_width * 2, ) - with pytest.warns(DeprecationWarning) as log: - assert font.getsize("A", stroke_width=stroke_width) == ( - 12 + stroke_width * 2, - 16 + stroke_width * 2, - ) - assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( - 48 + stroke_width * 2, - 36 + stroke_width * 4, - ) - assert len(log) == 2 + assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( + 48 + stroke_width * 2, + 36 + stroke_width * 4, + ) + assert len(log) == 2 def test_complex_font_settings(): diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 6de95306836..29c71f917c4 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -65,14 +65,16 @@ def create_lut(): # create_lut() -def test_lut(): - for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): - lb = ImageMorph.LutBuilder(op_name=op) - assert lb.get_lut() is None +@pytest.mark.parametrize( + "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") +) +def test_lut(op): + lb = ImageMorph.LutBuilder(op_name=op) + assert lb.get_lut() is None - lut = lb.build_lut() - with open(f"Tests/images/{op}.lut", "rb") as f: - assert lut == bytearray(f.read()) + lut = lb.build_lut() + with open(f"Tests/images/{op}.lut", "rb") as f: + assert lut == bytearray(f.read()) def test_no_operator_loaded(): diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 55d7c94798f..3e147a9efec 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -45,10 +45,10 @@ def show_image(self, image, **options): not on_ci() or is_win32(), reason="Only run on CIs; hangs on Windows CIs", ) -def test_show(): - for mode in ("1", "I;16", "LA", "RGB", "RGBA"): - im = hopper(mode) - assert ImageShow.show(im) +@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) +def test_show(mode): + im = hopper(mode) + assert ImageShow.show(im) def test_show_without_viewers(): @@ -70,12 +70,12 @@ def test_viewer(): viewer.get_command(None) -def test_viewers(): - for viewer in ImageShow._viewers: - try: - viewer.get_command("test.jpg") - except NotImplementedError: - pass +@pytest.mark.parametrize("viewer", ImageShow._viewers) +def test_viewers(viewer): + try: + viewer.get_command("test.jpg") + except NotImplementedError: + pass def test_ipythonviewer(): @@ -95,14 +95,14 @@ def test_ipythonviewer(): not on_ci() or is_win32(), reason="Only run on CIs; hangs on Windows CIs", ) -def test_file_deprecated(tmp_path): +@pytest.mark.parametrize("viewer", ImageShow._viewers) +def test_file_deprecated(tmp_path, viewer): f = str(tmp_path / "temp.jpg") - for viewer in ImageShow._viewers: - hopper().save(f) - with pytest.warns(DeprecationWarning): - try: - viewer.show_file(file=f) - except NotImplementedError: - pass - with pytest.raises(TypeError): - viewer.show_file() + hopper().save(f) + with pytest.warns(DeprecationWarning): + try: + viewer.show_file(file=f) + except NotImplementedError: + pass + with pytest.raises(TypeError): + viewer.show_file() diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index a848c786f04..995d0ee1f38 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -54,19 +54,19 @@ def test_kw(): assert im is None -def test_photoimage(): - for mode in TK_MODES: - # test as image: - im = hopper(mode) +@pytest.mark.parametrize("mode", TK_MODES) +def test_photoimage(mode): + # test as image: + im = hopper(mode) - # this should not crash - im_tk = ImageTk.PhotoImage(im) + # this should not crash + im_tk = ImageTk.PhotoImage(im) - assert im_tk.width() == im.width - assert im_tk.height() == im.height + assert im_tk.width() == im.width + assert im_tk.height() == im.height - reloaded = ImageTk.getimage(im_tk) - assert_image_equal(reloaded, im.convert("RGBA")) + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded, im.convert("RGBA")) def test_photoimage_apply_transparency(): @@ -76,17 +76,17 @@ def test_photoimage_apply_transparency(): assert_image_equal(reloaded, im.convert("RGBA")) -def test_photoimage_blank(): +@pytest.mark.parametrize("mode", TK_MODES) +def test_photoimage_blank(mode): # test a image using mode/size: - for mode in TK_MODES: - im_tk = ImageTk.PhotoImage(mode, (100, 100)) + im_tk = ImageTk.PhotoImage(mode, (100, 100)) - assert im_tk.width() == 100 - assert im_tk.height() == 100 + assert im_tk.width() == 100 + assert im_tk.height() == 100 - im = Image.new(mode, (100, 100)) - reloaded = ImageTk.getimage(im_tk) - assert_image_equal(reloaded.convert(mode), im) + im = Image.new(mode, (100, 100)) + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded.convert(mode), im) def test_box_deprecation(): diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 6e8a2ac589f..efcdab9ec43 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import hopper @@ -20,65 +22,56 @@ def verify(im1): ), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}" -def test_basic(tmp_path): +@pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) +def test_basic(tmp_path, mode): # PIL 1.1 has limited support for 16-bit image data. Check that # create/copy/transform and save works as expected. - def basic(mode): - - im_in = original.convert(mode) - verify(im_in) - - w, h = im_in.size + im_in = original.convert(mode) + verify(im_in) - im_out = im_in.copy() - verify(im_out) # copy + w, h = im_in.size - im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) - verify(im_out) # transform + im_out = im_in.copy() + verify(im_out) # copy - filename = str(tmp_path / "temp.im") - im_in.save(filename) + im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) + verify(im_out) # transform - with Image.open(filename) as im_out: - - verify(im_in) - verify(im_out) - - im_out = im_in.crop((0, 0, w, h)) - verify(im_out) + filename = str(tmp_path / "temp.im") + im_in.save(filename) - im_out = Image.new(mode, (w, h), None) - im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0)) - im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0)) + with Image.open(filename) as im_out: verify(im_in) verify(im_out) - im_in = Image.new(mode, (1, 1), 1) - assert im_in.getpixel((0, 0)) == 1 + im_out = im_in.crop((0, 0, w, h)) + verify(im_out) - im_in.putpixel((0, 0), 2) - assert im_in.getpixel((0, 0)) == 2 + im_out = Image.new(mode, (w, h), None) + im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0)) + im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0)) - if mode == "L": - maximum = 255 - else: - maximum = 32767 + verify(im_in) + verify(im_out) - im_in = Image.new(mode, (1, 1), 256) - assert im_in.getpixel((0, 0)) == min(256, maximum) + im_in = Image.new(mode, (1, 1), 1) + assert im_in.getpixel((0, 0)) == 1 - im_in.putpixel((0, 0), 512) - assert im_in.getpixel((0, 0)) == min(512, maximum) + im_in.putpixel((0, 0), 2) + assert im_in.getpixel((0, 0)) == 2 - basic("L") + if mode == "L": + maximum = 255 + else: + maximum = 32767 - basic("I;16") - basic("I;16B") - basic("I;16L") + im_in = Image.new(mode, (1, 1), 256) + assert im_in.getpixel((0, 0)) == min(256, maximum) - basic("I") + im_in.putpixel((0, 0), 512) + assert im_in.getpixel((0, 0)) == min(512, maximum) def test_tobytes(): diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 9735837bcba..185e477ecc5 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -137,19 +137,9 @@ def test_save_tiff_uint16(): assert img_px[0, 0] == pixel_value -def test_to_array(): - def _to_array(mode, dtype): - img = hopper(mode) - - # Resize to non-square - img = img.crop((3, 0, 124, 127)) - assert img.size == (121, 127) - - np_img = numpy.array(img) - _test_img_equals_nparray(img, np_img) - assert np_img.dtype == dtype - - modes = [ +@pytest.mark.parametrize( + "mode, dtype", + ( ("L", numpy.uint8), ("I", numpy.int32), ("F", numpy.float32), @@ -163,10 +153,18 @@ def _to_array(mode, dtype): ("I;16B", ">u2"), ("I;16L", " Date: Mon, 3 Oct 2022 17:48:27 +1100 Subject: [PATCH 241/242] Enabled test --- Tests/test_file_libtiff.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 6015a8fec22..d9066c58912 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -510,11 +510,7 @@ def test_palette_save(self, im, tmp_path): assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) - def xtest_bw_compression_w_rgb(self, compression, tmp_path): - """This test passes, but when running all tests causes a failure due - to output on stderr from the error thrown by libtiff. We need to - capture that but not now""" - + def test_bw_compression_w_rgb(self, compression, tmp_path): im = hopper("RGB") out = str(tmp_path / "temp.tif") From 92cb0af71f6af611ae5be28a6bf42642cef24726 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Oct 2022 19:54:37 +1100 Subject: [PATCH 242/242] Do not import PIL.Image --- Tests/test_000_sanity.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index 59fbac527ed..3fd982474d4 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,19 +1,18 @@ -import PIL -import PIL.Image +from PIL import Image def test_sanity(): # Make sure we have the binary extension - PIL.Image.core.new("L", (100, 100)) + Image.core.new("L", (100, 100)) # Create an image and do stuff with it. - im = PIL.Image.new("1", (100, 100)) + im = Image.new("1", (100, 100)) assert (im.mode, im.size) == ("1", (100, 100)) assert len(im.tobytes()) == 1300 # Create images in all remaining major modes. - PIL.Image.new("L", (100, 100)) - PIL.Image.new("P", (100, 100)) - PIL.Image.new("RGB", (100, 100)) - PIL.Image.new("I", (100, 100)) - PIL.Image.new("F", (100, 100)) + Image.new("L", (100, 100)) + Image.new("P", (100, 100)) + Image.new("RGB", (100, 100)) + Image.new("I", (100, 100)) + Image.new("F", (100, 100))