From 7f977f65229cd7037bfc4896fccc22b2c279d749 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 May 2024 09:10:28 +1000 Subject: [PATCH 01/66] Updated xz to 5.6.2 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0d45d5a209d..0df3bf17930 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -20,7 +20,7 @@ HARFBUZZ_VERSION=8.4.0 LIBPNG_VERSION=1.6.43 JPEGTURBO_VERSION=3.0.2 OPENJPEG_VERSION=2.5.2 -XZ_VERSION=5.4.5 +XZ_VERSION=5.6.2 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 if [[ -n "$IS_MACOS" ]]; then diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0d6da77549f..1df8db17bc7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,7 +120,7 @@ def cmd_msbuild( "LIBWEBP": "1.3.2", "OPENJPEG": "2.5.2", "TIFF": "4.6.0", - "XZ": "5.4.5", + "XZ": "5.6.2", "ZLIB": "1.3.1", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") From 142cb11dcce7b82df56c018a457e6d9876c7397e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 13 Jun 2024 10:58:51 +1000 Subject: [PATCH 02/66] Use GitHub URL for xz --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1df8db17bc7..0bc739ea816 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -176,7 +176,7 @@ def cmd_msbuild( "libs": [r"*.lib"], }, "xz": { - "url": f"{SF_PROJECTS}/lzmautils/files/xz-{V['XZ']}.tar.gz/download", + "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz", "filename": f"xz-{V['XZ']}.tar.gz", "dir": f"xz-{V['XZ']}", "license": "COPYING", From 4cf5688cfd42caed6f25db2d5ea29271ea8c1c9f Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 2 Jul 2024 16:43:34 +0400 Subject: [PATCH 03/66] Optimize getbbox() and getextrema() --- src/libImaging/GetBBox.c | 62 +++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index c61a27d3b7f..680d1cf77ba 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -30,26 +30,45 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[1] = -1; bbox[2] = bbox[3] = 0; -#define GETBBOX(image, mask) \ - for (y = 0; y < im->ysize; y++) { \ - has_data = 0; \ - for (x = 0; x < im->xsize; x++) { \ - if (im->image[y][x] & mask) { \ - has_data = 1; \ - if (x < bbox[0]) { \ - bbox[0] = x; \ - } \ - if (x >= bbox[2]) { \ - bbox[2] = x + 1; \ - } \ - } \ - } \ - if (has_data) { \ - if (bbox[1] < 0) { \ - bbox[1] = y; \ - } \ - bbox[3] = y + 1; \ - } \ +#define GETBBOX(image, mask) \ + /* first stage: looking for any px from top */ \ + for (y = 0; y < im->ysize; y++) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + bbox[0] = x; \ + bbox[1] = y; \ + break; \ + } \ + if (has_data) break; \ + } \ + /* Check that we got a box */ \ + if (bbox[1] < 0) \ + return 0; /* no data */ \ + /* second stage: looking for any px from bottom */ \ + for (y = im->ysize - 1; y >= bbox[1]; y--) { \ + has_data = 0; \ + for (x = 0; x < im->xsize; x++) \ + if (im->image[y][x] & mask) { \ + has_data = 1; \ + bbox[3] = y + 1; \ + break; \ + } \ + if (has_data) break; \ + } \ + /* third stage: looking for left and right boundaries */ \ + for (y = bbox[1]; y < bbox[3]; y++) { \ + for (x = 0; x < bbox[0]; x++) \ + if (im->image[y][x] & mask) { \ + bbox[0] = x; \ + break; \ + } \ + for (x = im->xsize - 1; x >= bbox[2]; x--) \ + if (im->image[y][x] & mask) { \ + bbox[2] = x + 1; \ + break; \ + } \ } if (im->image8) { @@ -144,6 +163,9 @@ ImagingGetExtrema(Imaging im, void *extrema) { imax = in[x]; } } + if (imin == 0 && imax == 255) { + break; + } } ((UINT8 *)extrema)[0] = (UINT8)imin; ((UINT8 *)extrema)[1] = (UINT8)imax; From 6a87df2c1fdd0dea1a3bdd5fb85e388e535e063b Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 2 Jul 2024 18:40:19 +0400 Subject: [PATCH 04/66] clang-format --- src/libImaging/GetBBox.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 680d1cf77ba..46a67807826 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -30,7 +30,7 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[1] = -1; bbox[2] = bbox[3] = 0; -#define GETBBOX(image, mask) \ +#define GETBBOX(image, mask) \ /* first stage: looking for any px from top */ \ for (y = 0; y < im->ysize; y++) { \ has_data = 0; \ @@ -41,7 +41,8 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[1] = y; \ break; \ } \ - if (has_data) break; \ + if (has_data) \ + break; \ } \ /* Check that we got a box */ \ if (bbox[1] < 0) \ @@ -55,7 +56,8 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[3] = y + 1; \ break; \ } \ - if (has_data) break; \ + if (has_data) \ + break; \ } \ /* third stage: looking for left and right boundaries */ \ for (y = bbox[1]; y < bbox[3]; y++) { \ From 5fb44ab694bc5576c6cd7e7ee54dfce660f497af Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 2 Jul 2024 18:45:13 +0400 Subject: [PATCH 05/66] This check is useless, since moved after the first stage --- src/libImaging/GetBBox.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 46a67807826..2e8afc8d54d 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -92,11 +92,6 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { GETBBOX(image32, mask); } - /* Check that we got a box */ - if (bbox[1] < 0) { - return 0; /* no data */ - } - return 1; /* ok */ } From 8256b9bb7fd22abfe61ed0013c02fa18255f398f Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Tue, 2 Jul 2024 19:05:24 +0400 Subject: [PATCH 06/66] Correct left boundary on the second stage --- src/libImaging/GetBBox.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 2e8afc8d54d..50b2253a0e0 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -53,6 +53,8 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { for (x = 0; x < im->xsize; x++) \ if (im->image[y][x] & mask) { \ has_data = 1; \ + if (x < bbox[0]) \ + bbox[0] = x; \ bbox[3] = y + 1; \ break; \ } \ From 2b6c5a27a1a6813fef4f06d45cf63428b2ae2da6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jul 2024 22:42:52 +1000 Subject: [PATCH 07/66] Added braces --- src/libImaging/GetBBox.c | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 50b2253a0e0..1303617ad1e 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -34,45 +34,53 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { /* first stage: looking for any px from top */ \ for (y = 0; y < im->ysize; y++) { \ has_data = 0; \ - for (x = 0; x < im->xsize; x++) \ + for (x = 0; x < im->xsize; x++) { \ if (im->image[y][x] & mask) { \ has_data = 1; \ bbox[0] = x; \ bbox[1] = y; \ break; \ } \ - if (has_data) \ + } \ + if (has_data) { \ break; \ + } \ } \ /* Check that we got a box */ \ - if (bbox[1] < 0) \ + if (bbox[1] < 0) { \ return 0; /* no data */ \ + } \ /* second stage: looking for any px from bottom */ \ for (y = im->ysize - 1; y >= bbox[1]; y--) { \ has_data = 0; \ - for (x = 0; x < im->xsize; x++) \ + for (x = 0; x < im->xsize; x++) { \ if (im->image[y][x] & mask) { \ has_data = 1; \ - if (x < bbox[0]) \ + if (x < bbox[0]) { \ bbox[0] = x; \ + } \ bbox[3] = y + 1; \ break; \ } \ - if (has_data) \ + } \ + if (has_data) { \ break; \ + } \ } \ /* third stage: looking for left and right boundaries */ \ for (y = bbox[1]; y < bbox[3]; y++) { \ - for (x = 0; x < bbox[0]; x++) \ + for (x = 0; x < bbox[0]; x++) { \ if (im->image[y][x] & mask) { \ bbox[0] = x; \ break; \ } \ - for (x = im->xsize - 1; x >= bbox[2]; x--) \ + } \ + for (x = im->xsize - 1; x >= bbox[2]; x--) { \ if (im->image[y][x] & mask) { \ bbox[2] = x + 1; \ break; \ } \ + } \ } if (im->image8) { From e4e2cd65642fa3dc02793fe426ba22a00d163db9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jul 2024 22:45:10 +1000 Subject: [PATCH 08/66] Updated comments --- src/libImaging/GetBBox.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index 1303617ad1e..d430893ddb2 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -31,7 +31,7 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { bbox[2] = bbox[3] = 0; #define GETBBOX(image, mask) \ - /* first stage: looking for any px from top */ \ + /* first stage: looking for any pixels from top */ \ for (y = 0; y < im->ysize; y++) { \ has_data = 0; \ for (x = 0; x < im->xsize; x++) { \ @@ -46,11 +46,11 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { break; \ } \ } \ - /* Check that we got a box */ \ + /* Check that we have a box */ \ if (bbox[1] < 0) { \ return 0; /* no data */ \ } \ - /* second stage: looking for any px from bottom */ \ + /* second stage: looking for any pixels from bottom */ \ for (y = im->ysize - 1; y >= bbox[1]; y--) { \ has_data = 0; \ for (x = 0; x < im->xsize; x++) { \ From cdadf931e36cb3816096197ce91abfbe6b11df92 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Fri, 30 Aug 2024 20:36:22 +0000 Subject: [PATCH 09/66] Improve error messages --- Tests/test_imagefont.py | 27 +++++++++++++++++++++++++++ src/PIL/ImageFont.py | 19 +++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 340cc47420b..e2a7b759ad5 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -460,6 +460,17 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) +def test_load_raises_if_image_not_found(tmp_path) -> None: + font_path = tmp_path / "file.font" + font_path.write_bytes(b"") + with pytest.raises(OSError) as excinfo: + ImageFont.load(font_path) + + pre = tmp_path / "file" + msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" + assert msg in str(excinfo.value) + + def test_load_path_not_found() -> None: # Arrange filename = "somefilenamethatdoesntexist.ttf" @@ -471,6 +482,22 @@ def test_load_path_not_found() -> None: ImageFont.truetype(filename) +def test_load_path_exisitng_path(tmp_path) -> None: + # First, the file doens't exist, so we don't suggest `load` + some_path = tmp_path / "file.ttf" + with pytest.raises(OSError) as excinfo: + ImageFont.load_path(str(some_path)) + assert str(some_path) in str(excinfo.value) + assert "did you mean" not in str(excinfo.value) + + # The file exists, so the error message suggests to use `load` instead + some_path.write_bytes(b"") + with pytest.raises(OSError) as excinfo: + ImageFont.load_path(str(some_path)) + assert str(some_path) in str(excinfo.value) + assert " did you mean" in str(excinfo.value) + + def test_load_non_font_bytes() -> None: with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2ab65bfefe4..072acc31f31 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -98,11 +98,13 @@ class ImageFont: def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: image: ImageFile.ImageFile | None = None + filename_body = os.path.splitext(filename)[0] + for ext in (".png", ".gif", ".pbm"): if image: image.close() try: - fullname = os.path.splitext(filename)[0] + ext + fullname = filename_body + ext image = Image.open(fullname) except Exception: pass @@ -112,7 +114,9 @@ def _load_pilfont(self, filename: str) -> None: else: if image: image.close() - msg = "cannot find glyph data file" + + pre = filename_body + msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" raise OSError(msg) self.file = fullname @@ -224,7 +228,7 @@ def __init__( raise core.ex if size <= 0: - msg = "font size must be greater than 0" + msg = f"font size must be greater than 0, not {size}" raise ValueError(msg) self.path = font @@ -774,6 +778,8 @@ def load(filename: str) -> ImageFont: :param filename: Name of font file. :return: A font object. :exception OSError: If the file could not be read. + + .. seealso:: :py:func:`PIL.ImageFont.truetype` """ f = ImageFont() f._load_pilfont(filename) @@ -850,6 +856,8 @@ def truetype( :return: A font object. :exception OSError: If the file could not be read. :exception ValueError: If the font size is not greater than zero. + + .. seealso:: :py:func:`PIL.ImageFont.load` """ def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: @@ -927,7 +935,10 @@ def load_path(filename: str | bytes) -> ImageFont: return load(os.path.join(directory, filename)) except OSError: pass - msg = "cannot find font file" + msg = f"cannot find font file '{filename}' in `sys.path`" + if os.path.exists(filename): + msg += f" did you mean `ImageFont.load({filename})` instead?" + raise OSError(msg) From 95194a2050081169f0b7db02040da91b1994da5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Aug 2024 20:51:26 +1000 Subject: [PATCH 10/66] Use tempfile.NamedTemporaryFile --- Tests/test_imagefont.py | 45 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index e2a7b759ad5..80cd0d578a5 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -5,6 +5,7 @@ import re import shutil import sys +import tempfile from io import BytesIO from pathlib import Path from typing import Any, BinaryIO @@ -460,15 +461,15 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) -def test_load_raises_if_image_not_found(tmp_path) -> None: - font_path = tmp_path / "file.font" - font_path.write_bytes(b"") - with pytest.raises(OSError) as excinfo: - ImageFont.load(font_path) +def test_load_when_image_not_found(tmp_path: Path) -> None: + tmpfile = tmp_path / "file.font" + tmpfile.write_bytes(b"") + tempfile = str(tmpfile) + with pytest.raises(OSError) as e: + ImageFont.load(tempfile) - pre = tmp_path / "file" - msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" - assert msg in str(excinfo.value) + root = os.path.splitext(tempfile)[0] + assert str(e.value) == f"cannot find glyph data file {root}.{{png|gif|pbm}}" def test_load_path_not_found() -> None: @@ -476,26 +477,24 @@ def test_load_path_not_found() -> None: filename = "somefilenamethatdoesntexist.ttf" # Act/Assert - with pytest.raises(OSError): + with pytest.raises(OSError) as e: ImageFont.load_path(filename) + + # The file doesn't exist, so don't suggest `load` + assert filename in str(e.value) + assert "did you mean" not in str(e.value) with pytest.raises(OSError): ImageFont.truetype(filename) -def test_load_path_exisitng_path(tmp_path) -> None: - # First, the file doens't exist, so we don't suggest `load` - some_path = tmp_path / "file.ttf" - with pytest.raises(OSError) as excinfo: - ImageFont.load_path(str(some_path)) - assert str(some_path) in str(excinfo.value) - assert "did you mean" not in str(excinfo.value) - - # The file exists, so the error message suggests to use `load` instead - some_path.write_bytes(b"") - with pytest.raises(OSError) as excinfo: - ImageFont.load_path(str(some_path)) - assert str(some_path) in str(excinfo.value) - assert " did you mean" in str(excinfo.value) +def test_load_path_existing_path() -> None: + with tempfile.NamedTemporaryFile() as tmp: + with pytest.raises(OSError) as e: + ImageFont.load_path(tmp.name) + + # The file exists, so the error message suggests to use `load` instead + assert tmp.name in str(e.value) + assert " did you mean" in str(e.value) def test_load_non_font_bytes() -> None: From e0a75b6d695b7be82c090adb1896022e42d344ff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Aug 2024 19:43:35 +1000 Subject: [PATCH 11/66] Renamed variable for first part of splitext to root --- src/PIL/ImageFont.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 072acc31f31..43e86a7ed25 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -98,13 +98,13 @@ class ImageFont: def _load_pilfont(self, filename: str) -> None: with open(filename, "rb") as fp: image: ImageFile.ImageFile | None = None - filename_body = os.path.splitext(filename)[0] + root = os.path.splitext(filename)[0] for ext in (".png", ".gif", ".pbm"): if image: image.close() try: - fullname = filename_body + ext + fullname = root + ext image = Image.open(fullname) except Exception: pass @@ -115,8 +115,7 @@ def _load_pilfont(self, filename: str) -> None: if image: image.close() - pre = filename_body - msg = f"cannot find glyph data file {pre}.{{png|gif|pbm}}" + msg = f"cannot find glyph data file {root}.{{png|gif|pbm}}" raise OSError(msg) self.file = fullname @@ -937,7 +936,7 @@ def load_path(filename: str | bytes) -> ImageFont: pass msg = f"cannot find font file '{filename}' in `sys.path`" if os.path.exists(filename): - msg += f" did you mean `ImageFont.load({filename})` instead?" + msg += f", did you mean `ImageFont.load({filename})` instead?" raise OSError(msg) From fcca8a3059f94640dee493164b54ec466c13ff74 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sat, 31 Aug 2024 19:03:11 +0000 Subject: [PATCH 12/66] Fix accidental indent --- Tests/test_imagefont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index e2a7b759ad5..80ec7bd6b32 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -487,8 +487,8 @@ def test_load_path_exisitng_path(tmp_path) -> None: some_path = tmp_path / "file.ttf" with pytest.raises(OSError) as excinfo: ImageFont.load_path(str(some_path)) - assert str(some_path) in str(excinfo.value) - assert "did you mean" not in str(excinfo.value) + assert str(some_path) in str(excinfo.value) + assert "did you mean" not in str(excinfo.value) # The file exists, so the error message suggests to use `load` instead some_path.write_bytes(b"") From ef51e7a1c70ae684615724750c32ba3ca6923123 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Sun, 1 Sep 2024 19:49:44 +0000 Subject: [PATCH 13/66] Fix wrong indentation for assert --- Tests/test_imagefont.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 80cd0d578a5..66e6947c2be 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -480,9 +480,9 @@ def test_load_path_not_found() -> None: with pytest.raises(OSError) as e: ImageFont.load_path(filename) - # The file doesn't exist, so don't suggest `load` - assert filename in str(e.value) - assert "did you mean" not in str(e.value) + # The file doesn't exist, so don't suggest `load` + assert filename in str(e.value) + assert "did you mean" not in str(e.value) with pytest.raises(OSError): ImageFont.truetype(filename) From e625f7311965abd0a88992ca215d8ad61a27715e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 11:37:12 +1000 Subject: [PATCH 14/66] Added scale argument to IcnsImageFile load() --- Tests/test_file_icns.py | 20 ++++++++---- docs/handbook/image-file-formats.rst | 12 +++++++ src/PIL/IcnsImagePlugin.py | 49 ++++++++++++++-------------- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 488984aef70..f37a6996fa4 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -63,8 +63,8 @@ def test_save_append_images(tmp_path: Path) -> None: assert_image_similar_tofile(im, temp_file, 1) with Image.open(temp_file) as reread: - reread.size = (16, 16, 2) - reread.load() + reread.size = (16, 16) + reread.load(2) assert_image_equal(reread, provided_im) @@ -92,9 +92,15 @@ def test_sizes() -> None: assert im.mode == "RGBA" assert im.size == (wr, hr) + # Test using load() with scale + im.size = (w, h) + im.load(scale=r) + assert im.mode == "RGBA" + assert im.size == (wr, hr) + # Check that we cannot load an incorrect size with pytest.raises(ValueError): - im.size = (1, 1) + im.size = (1, 2) def test_older_icon() -> None: @@ -105,8 +111,8 @@ def test_older_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow2.icns") as im2: - im2.size = (w, h, r) - im2.load() + im2.size = (w, h) + im2.load(r) assert im2.mode == "RGBA" assert im2.size == (wr, hr) @@ -122,8 +128,8 @@ def test_jp2_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow3.icns") as im2: - im2.size = (w, h, r) - im2.load() + im2.size = (w, h) + im2.load(r) assert im2.mode == "RGBA" assert im2.size == (wr, hr) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index ca0e05eb6c0..a5fbc80533b 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -331,6 +331,18 @@ 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-loading: + +Loading +~~~~~~~ + +You can call the :py:meth:`~PIL.Image.Image.load` method with the following parameter. + +**scale** + Affects the scale of the resultant image. If the size is set to ``(512, 512)``, + after loading at scale 2, the final value of :py:attr:`~PIL.Image.Image.size` will + be ``(1024, 1024)``. + .. _icns-saving: Saving diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index ca66aa0fd1e..e4f486a4f77 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -281,31 +281,30 @@ def size(self): @size.setter def size(self, value) -> None: - info_size = value - if info_size not in self.info["sizes"] and len(info_size) == 2: - info_size = (info_size[0], info_size[1], 1) - if ( - info_size not in self.info["sizes"] - and len(info_size) == 3 - and info_size[2] == 1 - ): - simple_sizes = [ - (size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"] - ] - if value in simple_sizes: - info_size = self.info["sizes"][simple_sizes.index(value)] - if info_size not in self.info["sizes"]: - msg = "This is not one of the allowed sizes of this image" - raise ValueError(msg) - self._size = value - - def load(self) -> Image.core.PixelAccess | None: - if len(self.size) == 3: - self.best_size = self.size - self.size = ( - self.best_size[0] * self.best_size[2], - self.best_size[1] * self.best_size[2], - ) + if len(value) == 3: + if value in self.info["sizes"]: + self._size = value + return + else: + # Check that a matching size exists, + # or that there is a scale that would create a size that matches + for size in self.info["sizes"]: + simple_size = size[0] * size[2], size[1] * size[2] + scale = simple_size[0] // value[0] + if simple_size[1] / value[1] == scale: + self._size = value + return + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) + + def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: + if scale is not None or len(self.size) == 3: + if scale is None and len(self.size) == 3: + scale = self.size[2] + assert scale is not None + width, height = self.size[:2] + self.size = width * scale, height * scale + self.best_size = width, height, scale px = Image.Image.load(self) if self._im is not None and self.im.size == self.size: From a6f5f4dd43c95ded41da5d56c7e9782f54e8dea2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2024 11:42:56 +1000 Subject: [PATCH 15/66] Deprecate setting size to (width, height, scale) --- Tests/test_file_icns.py | 3 ++- docs/deprecations.rst | 8 ++++++++ docs/handbook/image-file-formats.rst | 7 +------ docs/releasenotes/11.0.0.rst | 13 +++++++++++++ src/PIL/IcnsImagePlugin.py | 10 ++++++---- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index f37a6996fa4..16f6b36516d 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -87,7 +87,8 @@ def test_sizes() -> None: for w, h, r in im.info["sizes"]: wr = w * r hr = h * r - im.size = (w, h, r) + with pytest.warns(DeprecationWarning): + im.size = (w, h, r) im.load() assert im.mode == "RGBA" assert im.size == (wr, hr) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a9498d5ed5e..061f579dee9 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -109,6 +109,14 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. +ICNS (width, height, scale) sizes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Setting an ICNS image size to ``(width, height, scale)`` before loading has been +deprecated. Instead, ``load(scale)`` can be used. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a5fbc80533b..8183473e4f5 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -324,12 +324,7 @@ sets the following :py:attr:`~PIL.Image.Image.info` property: **sizes** A list of supported sizes found in this icon file; these are a 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina - icon and 1 for a standard icon. You *are* permitted to use this 3-tuple - format for the :py:attr:`~PIL.Image.Image.size` property if you set it - before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size - will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you - ask for ``(512, 512, 2)``, the final value of - :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). + icon and 1 for a standard icon. .. _icns-loading: diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index ac9237acf78..27c1bd30831 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -43,9 +43,20 @@ similarly removed. Deprecations ============ + +ICNS (width, height, scale) sizes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Setting an ICNS image size to ``(width, height, scale)`` before loading has been +deprecated. Instead, ``load(scale)`` can be used. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. deprecated:: 11.0.0 + The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword arguments can be used instead. @@ -61,6 +72,8 @@ have been deprecated, and will be removed in Pillow 12 (2025-10-15). Specific WebP Feature Checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. deprecated:: 11.0.0 + ``features.check("transp_webp")``, ``features.check("webp_mux")`` and ``features.check("webp_anim")`` are now deprecated. They will always return ``True`` if the WebP module is installed, until they are removed in Pillow diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index e4f486a4f77..9757b2b142d 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -25,6 +25,7 @@ from typing import IO from . import Image, ImageFile, PngImagePlugin, features +from ._deprecate import deprecate enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: @@ -275,15 +276,16 @@ def _open(self) -> None: self.best_size[1] * self.best_size[2], ) - @property - def size(self): + @property # type: ignore[override] + def size(self) -> tuple[int, int] | tuple[int, int, int]: return self._size @size.setter - def size(self, value) -> None: + def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None: if len(value) == 3: + deprecate("Setting size to (width, height, scale)", 12, "load(scale)") if value in self.info["sizes"]: - self._size = value + self._size = value # type: ignore[assignment] return else: # Check that a matching size exists, From d3c1d99d00b3d76d9c3dbdfc629b6278a0e38613 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:22:39 +0300 Subject: [PATCH 16/66] Deprecate support for FreeType 2.9.0 --- Tests/test_imagefont.py | 12 ++++++++++++ docs/deprecations.rst | 13 +++++++++++++ docs/releasenotes/11.0.0.rst | 13 +++++++++++++ src/PIL/ImageFont.py | 17 ++++++++++++++++- 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 953706010f6..cfadc7312a1 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1150,3 +1150,15 @@ def test_invalid_truetype_sizes_raise_valueerror( ) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) + + +def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None: + # Arrange: mock features.version_module to return fake FreeType version + def fake_version_module(module): + return "2.9.0" + + monkeypatch.setattr(features, "version_module", fake_version_module) + + # Act / Assert + with pytest.warns(DeprecationWarning): + ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a9498d5ed5e..9a2f16cfb68 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,6 +12,19 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. +FreeType 2.9.0 +~~~~~~~~~~~~~~ + +.. deprecated:: 11.0.0 + +Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 +(2025-10-15), when FreeType 2.9.1 will be 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`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + ImageFile.raise_oserror ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index ac9237acf78..99e0a292766 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -43,6 +43,19 @@ similarly removed. Deprecations ============ +FreeType 2.9.0 +^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 +(2025-10-15), when FreeType 2.9.1 will be 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`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a82c36ba6b8..50796e132dc 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -36,7 +36,7 @@ from types import ModuleType from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast -from . import Image +from . import Image, features from ._typing import StrOrBytesPath from ._util import DeferredError, is_path @@ -232,6 +232,21 @@ def __init__( self.index = index self.encoding = encoding + try: + from packaging.version import parse as parse_version + except ImportError: + pass + else: + if freetype_version := features.version_module("freetype2"): + if parse_version(freetype_version) < parse_version("2.9.1"): + warnings.warn( + "Support for FreeType 2.9.0 is deprecated and will be removed " + "in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 " + "or newer, preferably FreeType 2.10.4 which fixes " + "CVE-2020-15999.", + DeprecationWarning, + ) + if layout_engine not in (Layout.BASIC, Layout.RAQM): layout_engine = Layout.BASIC if core.HAVE_RAQM: From de3c6fa2957cd237cea024ae558034379129362e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 12:14:30 +1000 Subject: [PATCH 17/66] Support converting more modes to LAB by converting to RGBA first --- Tests/test_imagecms.py | 6 ++++++ src/PIL/Image.py | 14 ++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 5ee5fcedf24..f062651f0c5 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -696,6 +696,12 @@ def test_rgb_lab(mode: str) -> None: assert value[:3] == (0, 255, 255) +def test_cmyk_lab() -> None: + im = Image.new("CMYK", (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (255, 128, 128) + + def test_deprecation() -> None: with pytest.warns(DeprecationWarning): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1c0cf293633..d706d2d3352 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1127,17 +1127,23 @@ def convert_transparency( return new_im if "LAB" in (self.mode, mode): - other_mode = mode if self.mode == "LAB" else self.mode + im = self + if mode == "LAB": + if im.mode not in ("RGB", "RGBA", "RGBX"): + im = im.convert("RGBA") + other_mode = im.mode + else: + other_mode = mode if other_mode in ("RGB", "RGBA", "RGBX"): from . import ImageCms srgb = ImageCms.createProfile("sRGB") lab = ImageCms.createProfile("LAB") - profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab] + profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab] transform = ImageCms.buildTransform( - profiles[0], profiles[1], self.mode, mode + profiles[0], profiles[1], im.mode, mode ) - return transform.apply(self) + return transform.apply(im) # colorspace conversion if dither is None: From e14072e9738275ffe2898f32d36cbd2f3c687cf3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:08:07 +1000 Subject: [PATCH 18/66] Added further detail --- src/PIL/ImageFont.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 43e86a7ed25..abfa4aa80fb 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -771,14 +771,13 @@ def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float: def load(filename: str) -> ImageFont: """ - Load a font file. This function loads a font object from the given - bitmap font file, and returns the corresponding font object. + Load a font file. This function loads a font object from the given + bitmap font file, and returns the corresponding font object. For loading TrueType + or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`. :param filename: Name of font file. :return: A font object. :exception OSError: If the file could not be read. - - .. seealso:: :py:func:`PIL.ImageFont.truetype` """ f = ImageFont() f._load_pilfont(filename) @@ -794,7 +793,8 @@ def truetype( ) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, - and create a font object. + and create a font object. For loading bitmap fonts instead, + see :py:func:`~PIL.ImageFont.load` and :py:func:`~PIL.ImageFont.load_path`. 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. @@ -855,8 +855,6 @@ def truetype( :return: A font object. :exception OSError: If the file could not be read. :exception ValueError: If the font size is not greater than zero. - - .. seealso:: :py:func:`PIL.ImageFont.load` """ def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont: From dbe979d032afed04b93f9ce1aaa9c7f8e23542b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:09:01 +1000 Subject: [PATCH 19/66] Sort extensions alphabetically in error message --- Tests/test_imagefont.py | 2 +- src/PIL/ImageFont.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 66e6947c2be..32cd1db8a70 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -469,7 +469,7 @@ def test_load_when_image_not_found(tmp_path: Path) -> None: ImageFont.load(tempfile) root = os.path.splitext(tempfile)[0] - assert str(e.value) == f"cannot find glyph data file {root}.{{png|gif|pbm}}" + assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}" def test_load_path_not_found() -> None: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index abfa4aa80fb..b6d69019da8 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -115,7 +115,7 @@ def _load_pilfont(self, filename: str) -> None: if image: image.close() - msg = f"cannot find glyph data file {root}.{{png|gif|pbm}}" + msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}" raise OSError(msg) self.file = fullname From 2c02146cf4c745df531be49e6900005d7df486b7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:32:30 +0300 Subject: [PATCH 20/66] Use type hints and fix CVE role Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_imagefont.py | 2 +- docs/deprecations.rst | 2 +- docs/releasenotes/11.0.0.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index cfadc7312a1..3c916db1852 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1154,7 +1154,7 @@ def test_invalid_truetype_sizes_raise_valueerror( def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange: mock features.version_module to return fake FreeType version - def fake_version_module(module): + def fake_version_module(module: str) -> str: return "2.9.0" monkeypatch.setattr(features, "version_module", fake_version_module) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 9a2f16cfb68..8d303282778 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -21,7 +21,7 @@ Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 (2025-10-15), when FreeType 2.9.1 will be 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`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 99e0a292766..0c637cc6598 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -52,7 +52,7 @@ Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 (2025-10-15), when FreeType 2.9.1 will be 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`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ From 547832fd592ee1392533be5bf935c19c690a665f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2024 19:49:21 +1000 Subject: [PATCH 21/66] Use tempfile.NamedTemporaryFile --- Tests/test_imagefont.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 32cd1db8a70..e3d84756791 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -461,14 +461,15 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: assert mask.size == (108, 13) -def test_load_when_image_not_found(tmp_path: Path) -> None: - tmpfile = tmp_path / "file.font" - tmpfile.write_bytes(b"") - tempfile = str(tmpfile) +def test_load_when_image_not_found() -> None: + with tempfile.NamedTemporaryFile(delete=False) as tmp: + pass with pytest.raises(OSError) as e: - ImageFont.load(tempfile) + ImageFont.load(tmp.name) - root = os.path.splitext(tempfile)[0] + os.unlink(tmp.name) + + root = os.path.splitext(tmp.name)[0] assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}" @@ -492,8 +493,8 @@ def test_load_path_existing_path() -> None: with pytest.raises(OSError) as e: ImageFont.load_path(tmp.name) - # The file exists, so the error message suggests to use `load` instead - assert tmp.name in str(e.value) + # The file exists, so the error message suggests to use `load` instead + assert tmp.name in str(e.value) assert " did you mean" in str(e.value) From 6231453895ff9098fde1de8e4be2ddcf0574b1a8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 8 Sep 2024 21:53:08 +1000 Subject: [PATCH 22/66] Group 11.0.0 deprecations --- docs/deprecations.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8d303282778..bf290a9cda7 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,19 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. -FreeType 2.9.0 -~~~~~~~~~~~~~~ - -.. deprecated:: 11.0.0 - -Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 -(2025-10-15), when FreeType 2.9.1 will be the minimum supported. - -We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). - -.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ - ImageFile.raise_oserror ~~~~~~~~~~~~~~~~~~~~~~~ @@ -122,6 +109,19 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. +FreeType 2.9.0 +~~~~~~~~~~~~~~ + +.. deprecated:: 11.0.0 + +Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 +(2025-10-15), when FreeType 2.9.1 will be the minimum supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 8c738f6427c1be395921f5ef3f1e6e4aa86f4eb7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:00:15 +0000 Subject: [PATCH 23/66] Update deadsnakes/action action to v3.2.0 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b169db9170..e0903f8869c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: "pyproject.toml" - name: Set up Python ${{ matrix.python-version }} (free-threaded) - uses: deadsnakes/action@v3.1.0 + uses: deadsnakes/action@v3.2.0 if: "${{ matrix.disable-gil }}" with: python-version: ${{ matrix.python-version }} From 2f13c4588d6287140a7e94bc8bc6c732610bd31e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:17:13 +0300 Subject: [PATCH 24/66] Fix underline Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/deprecations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index bf290a9cda7..1d2da238129 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -110,7 +110,7 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. FreeType 2.9.0 -~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^ .. deprecated:: 11.0.0 From 608a5e83c067baa91e555b24b3cc577c3704153c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Sep 2024 12:56:28 +1000 Subject: [PATCH 25/66] Updated harfbuzz to 9.0.0, except on manylinux2014 --- .github/workflows/wheels-dependencies.sh | 47 +++++++++++++++++------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97f70ed84e9..c25e1a27d94 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,7 +16,11 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=8.5.0 +if [[ "$MB_ML_VER" != 2014 ]]; then + HARFBUZZ_VERSION=9.0.0 +else + HARFBUZZ_VERSION=8.5.0 +fi LIBPNG_VERSION=1.6.43 JPEGTURBO_VERSION=3.0.3 OPENJPEG_VERSION=2.5.2 @@ -40,7 +44,7 @@ BROTLI_VERSION=1.1.0 if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then function build_openjpeg { - local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) + local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v$OPENJPEG_VERSION.tar.gz openjpeg-$OPENJPEG_VERSION.tar.gz) (cd $out_dir \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) @@ -50,7 +54,7 @@ fi function build_brotli { local cmake=$(get_modern_cmake) - local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz) + local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) @@ -60,6 +64,25 @@ function build_brotli { fi } +function build_harfbuzz { + if [[ "$HARFBUZZ_VERSION" == 8.5.0 ]]; then + export FREETYPE_LIBS=-lfreetype + export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ + build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no + export FREETYPE_LIBS="" + export FREETYPE_CFLAGS="" + else + local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + (cd $out_dir \ + && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) + (cd $out_dir/build \ + && meson install) + if [[ "$MB_ML_LIBC" == "manylinux" ]]; then + cp /usr/local/lib64/libharfbuzz* /usr/local/lib + fi + fi +} + function build { if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then sudo chown -R runner /usr/local @@ -109,15 +132,7 @@ function build { build_freetype fi - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS=-lfreetype - export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ - fi - build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS="" - export FREETYPE_CFLAGS="" - fi + build_harfbuzz } # Any stuff that you need to do before you start building the wheels @@ -140,7 +155,13 @@ if [[ -n "$IS_MACOS" ]]; then brew remove --ignore-dependencies webp fi - brew install pkg-config + brew install meson pkg-config +elif [[ "$MB_ML_LIBC" == "manylinux" ]]; then + if [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then + yum install -y meson + fi +else + apk add meson fi wrap_wheel_builder build From c9dc34ae8d90e632d90a685857c80c516b1668f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Sep 2024 13:19:37 +1000 Subject: [PATCH 26/66] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index be94594f88c..15da95e0909 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Deprecate support for FreeType 2.9.0 #8356 + [hugovk, radarhere] + - Removed unused TiffImagePlugin IFD_LEGACY_API #8355 [radarhere] From be01d536c652f2224351ad0803c92067394db1bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 9 Sep 2024 15:22:27 +1000 Subject: [PATCH 27/66] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 15da95e0909..e217c9b82be 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Support converting more modes to LAB by converting to RGBA first #8358 + [radarhere] + - Deprecate support for FreeType 2.9.0 #8356 [hugovk, radarhere] From e0845f06d8d063fbf78708dd20a692041b60596c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 16:20:52 +1000 Subject: [PATCH 28/66] Deprecate isImageType --- Tests/test_image.py | 4 ++++ docs/deprecations.rst | 8 ++++++++ docs/releasenotes/11.0.0.rst | 8 ++++++++ src/PIL/Image.py | 17 +++++++---------- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 97e97acaabd..cf5b5a710cc 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1117,6 +1117,10 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: assert len(caplog.records) == 0 assert im.fp is None + def test_deprecation(self) -> None: + with pytest.warns(DeprecationWarning): + assert not Image.isImageType(None) + class TestImageBytes: @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8d1537a5dd5..57f7f5b5c3b 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -122,6 +122,14 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` +instead. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index dd19c4b7f85..e0be1169070 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -61,6 +61,14 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` +instead. + ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e6d9047f5b9..8f16eef94cf 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -133,6 +133,7 @@ def isImageType(t: Any) -> TypeGuard[Image]: :param t: object to check if it's an image :returns: True if the object is an image """ + deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)") return hasattr(t, "im") @@ -1823,23 +1824,22 @@ def paste( :param mask: An optional mask image. """ - if isImageType(box): + if isinstance(box, Image): if mask is not None: msg = "If using second argument as mask, third argument must be None" raise ValueError(msg) # abbreviated paste(im, mask) syntax mask = box box = None - assert not isinstance(box, Image) if box is None: box = (0, 0) if len(box) == 2: # upper left corner given; get size from image or mask - if isImageType(im): + if isinstance(im, Image): size = im.size - elif isImageType(mask): + elif isinstance(mask, Image): size = mask.size else: # FIXME: use self.size here? @@ -1852,17 +1852,15 @@ def paste( from . import ImageColor source = ImageColor.getcolor(im, self.mode) - elif isImageType(im): + elif isinstance(im, Image): im.load() if self.mode != im.mode: if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): # should use an adapter for this! im = im.convert(self.mode) source = im.im - elif isinstance(im, tuple): - source = im else: - source = cast(float, im) + source = im self._ensure_mutable() @@ -2023,7 +2021,7 @@ def putalpha(self, alpha: Image | int) -> None: else: band = 3 - if isImageType(alpha): + if isinstance(alpha, Image): # alpha layer if alpha.mode not in ("1", "L"): msg = "illegal image mode" @@ -2033,7 +2031,6 @@ def putalpha(self, alpha: Image | int) -> None: alpha = alpha.convert("L") else: # constant alpha - alpha = cast(int, alpha) # see python/typing#1013 try: self.im.fillband(band, alpha) except (AttributeError, ValueError): From adb691821476eed08bbefe2478794c5dbd94b527 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 11:38:56 +1000 Subject: [PATCH 29/66] Use isinstance to detect Image instances --- src/PIL/ImageMath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 191cc2a5f88..191ca8524cc 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -268,7 +268,7 @@ def lambda_eval( args.update(options) args.update(kw) for k, v in args.items(): - if hasattr(v, "im"): + if isinstance(v, Image.Image): args[k] = _Operand(v) out = expression(args) @@ -319,7 +319,7 @@ def unsafe_eval( args.update(options) args.update(kw) for k, v in args.items(): - if hasattr(v, "im"): + if isinstance(v, Image.Image): args[k] = _Operand(v) compiled_code = compile(expression, "", "eval") From d522e0a5c03f87b653ab25a23116d9f685aa0c1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 18:50:06 +1000 Subject: [PATCH 30/66] Improved handling of RGBA palettes when saving GIF images --- Tests/test_file_gif.py | 18 ++++++++++++++++++ src/PIL/GifImagePlugin.py | 17 ++++++++++++++--- src/PIL/Image.py | 5 ++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 571fe1b9ac0..16c8466f331 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1429,3 +1429,21 @@ def test_saving_rgba(tmp_path: Path) -> None: with Image.open(out) as reloaded: reloaded_rgba = reloaded.convert("RGBA") assert reloaded_rgba.load()[0, 0][3] == 0 + + +def test_optimizing_p_rgba(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im1 = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im1) + d.ellipse([(40, 40), (60, 60)], fill=1) + data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254 + im1.putpalette(data, "RGBA") + + im2 = Image.new("P", (100, 100)) + im2.putpalette(data, "RGBA") + + im1.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.n_frames == 2 diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f206fbb9cef..57c29179273 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -553,7 +553,9 @@ def _normalize_palette( if im.mode == "P": if not source_palette: - source_palette = im.im.getpalette("RGB")[:768] + im_palette = im.getpalette(None) + assert im_palette is not None + source_palette = bytearray(im_palette) else: # L-mode if not source_palette: source_palette = bytearray(i // 3 for i in range(768)) @@ -629,7 +631,10 @@ def _write_single_frame( def _getbbox( base_im: Image.Image, im_frame: Image.Image ) -> tuple[Image.Image, tuple[int, int, int, int] | None]: - if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): + palette_bytes = [ + bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame) + ] + if palette_bytes[0] != palette_bytes[1]: im_frame = im_frame.convert("RGBA") base_im = base_im.convert("RGBA") delta = ImageChops.subtract_modulo(im_frame, base_im) @@ -984,7 +989,13 @@ def _get_palette_bytes(im: Image.Image) -> bytes: :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return bytes(im.palette.palette) if im.palette else b"" + if not im.palette: + return b"" + + palette = bytes(im.palette.palette) + if im.palette.mode == "RGBA": + palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3)) + return palette def _get_background( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e6d9047f5b9..a7a158d35c7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1066,7 +1066,7 @@ def convert_transparency( trns_im = new(self.mode, (1, 1)) if self.mode == "P": assert self.palette is not None - trns_im.putpalette(self.palette) + trns_im.putpalette(self.palette, self.palette.mode) if isinstance(t, tuple): err = "Couldn't allocate a palette color for transparency" assert trns_im.palette is not None @@ -2182,6 +2182,9 @@ def remap_palette( source_palette = self.im.getpalette(palette_mode, palette_mode) else: # L-mode source_palette = bytearray(i // 3 for i in range(768)) + elif len(source_palette) > 768: + bands = 4 + palette_mode = "RGBA" palette_bytes = b"" new_positions = [0] * 256 From 8d508406ad8917919c5da084bf0e543146643522 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 20:55:34 +1000 Subject: [PATCH 31/66] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e217c9b82be..3e4a99be77f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Deprecate isImageType #8364 + [radarhere] + - Support converting more modes to LAB by converting to RGBA first #8358 [radarhere] From 54c5b532c54759d9a66541e8bb6ba0893b3c8cf6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 21:32:20 +1000 Subject: [PATCH 32/66] Added release notes for #8286 --- docs/releasenotes/11.0.0.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index e0be1169070..22dd5313bdd 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -103,10 +103,18 @@ TODO API Additions ============= -TODO -^^^^ +Writing XMP bytes to JPEG and MPO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +XMP data can now be saved to JPEG files using an ``xmp`` argument:: + + im.save("out.jpg", xmp=b"test") + +The data can also be set through :py:attr:`~PIL.Image.Image.info`, for use when saving +either JPEG or MPO images:: + + im.info["xmp"] = b"test" + im.save("out.jpg") Other Changes ============= From fea3e0dd5facd07c9133b1b5dbb7b97452d62411 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 21:22:03 +1000 Subject: [PATCH 33/66] Added release notes for #8213 --- docs/releasenotes/11.0.0.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 22dd5313bdd..a83e3f2e947 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -45,6 +45,11 @@ TiffImagePlugin IFD_LEGACY_API An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed. +WebP 0.4 +^^^^^^^^ + +Support for WebP 0.4 and earlier has been removed; WebP 0.5 is the minimum supported. + Deprecations ============ From 5c67178c952560c8992c2eaac4d62a5ca278f83d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 21:30:48 +1000 Subject: [PATCH 34/66] Added release notes for #8199 --- docs/releasenotes/11.0.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index a83e3f2e947..376130499c9 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -133,6 +133,8 @@ of 3.13.0 final (2024-10-01, :pep:`719`). Pillow 11.0.0 now officially supports Python 3.13. +Support has also been added for the experimental free-threaded mode of :pep:`703`. + C-level Flags ^^^^^^^^^^^^^ From bc4c57e9ea3089d168ac85b0f8c4e92db48e669d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 22:39:47 +1000 Subject: [PATCH 35/66] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3e4a99be77f..af292f7a135 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Deprecate ICNS (width, height, scale) sizes in favour of load(scale) #8352 + [radarhere] + +- Improved handling of RGBA palettes when saving GIF images #8366 + [radarhere] + - Deprecate isImageType #8364 [radarhere] From 6ade6873dec23cb6c411f1b1e8a405ada8b85a35 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Sep 2024 08:13:41 +1000 Subject: [PATCH 36/66] Removed libffi-dev --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index ee92d447a3e..7e3a691e566 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -21,7 +21,7 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ - ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ + ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ sway wl-clipboard libopenblas-dev fi From d2efd7dd5fcd19b7cb7768fc90ffa51676ff5e7a Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Wed, 11 Sep 2024 10:32:42 +0200 Subject: [PATCH 37/66] Update src/PIL/ImageFont.py 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 b6d69019da8..7ee3aaa55ec 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -934,7 +934,7 @@ def load_path(filename: str | bytes) -> ImageFont: pass msg = f"cannot find font file '{filename}' in `sys.path`" if os.path.exists(filename): - msg += f", did you mean `ImageFont.load({filename})` instead?" + msg += f", did you mean `ImageFont.load(\"{filename}\")` instead?" raise OSError(msg) From e4f13020e16925f1dcfe738218e4d03a3c3992bb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 08:33:09 +0000 Subject: [PATCH 38/66] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- 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 7ee3aaa55ec..55ab6195184 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -934,7 +934,7 @@ def load_path(filename: str | bytes) -> ImageFont: pass msg = f"cannot find font file '{filename}' in `sys.path`" if os.path.exists(filename): - msg += f", did you mean `ImageFont.load(\"{filename}\")` instead?" + msg += f', did you mean `ImageFont.load("{filename}")` instead?' raise OSError(msg) From 01ba1e2252bb6fab3bdee4ee0b2952746d919270 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Sep 2024 19:53:52 +1000 Subject: [PATCH 39/66] Accept float stroke widths --- Tests/images/imagedraw_stroke_float.png | Bin 0 -> 1509 bytes Tests/test_imagedraw.py | 14 ++++++++++++++ src/_imagingft.c | 18 +++++++++--------- 3 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 Tests/images/imagedraw_stroke_float.png diff --git a/Tests/images/imagedraw_stroke_float.png b/Tests/images/imagedraw_stroke_float.png new file mode 100644 index 0000000000000000000000000000000000000000..a590f8af0b616852c8d54e591d04265dea90447b GIT binary patch literal 1509 zcma)+dpi>f0Eb7mn8{^mF&s3vgt;u2C}ZZ7!A38(GopOnZ9cH6qE+N-r71fmL zkz`~jvY;v-<^-QkdSNMhcV&r?TbN@@SobhJcmaC8Hn=PkN zzerpf&46-HeRZoeb0Q=&%}6(H{ShMi)jeAJ;W`1r96?c`b&i?GQPuT2bk8-7i&xbL zc})(n%PNfw6nfGS&YMc4t4WM1c9{>1{;7m?1pz)45y~$DhnIXbnBNd<_i9^12X8ud zS6^`63@I9FhTqem}l${p+<T znpcwuxCqOwtUi>ATq$DB$4p9Q8&iKO-@|Eg#F1KVFdG7t;4y-aQ+H zI$Rg}OIEQfKCv4f3OXWfhy$F@K5M2!9OWIJ7-t)mQp0RxWAF!CVbi)LEQ!UNKSD1< zx(bXNje3ECbh|viOa1E(SJns_nsoi%f(@$L^PWLW<;VzAhLh>4BHb661WKGTu@PBC z58Z%D+D2}$e;*MBHq6a^+hP}5@4O9yRxI~9b<4q?P1r*SVfI|KW3Gh`Bf#{#cFrvx zz4}4+@7l$AbdO~CTgt4Z@(a1k1I^+B?e$UbzHg44f>Q@jURX_tZIJCCR96r{yH(we?{wxN&{QF6E{s|{Y$s)+q+%7VHj%gK|aq?UPp_x~WsLT&lk%aE6{kzXUYDZ(SkTJG1H zm@fW8XV#|U-jshaiQ67&sXc~G@n#oJOCn?j^ms0mlYQ?M&$(G3U>gP8G= zy;$!r None: ) +@skip_unless_feature("freetype2") +def test_stroke_float() -> None: + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text((12, 12), "A", "#f00", font, stroke_width=0.5) + + # Assert + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_float.png", 3.1) + + @skip_unless_feature("freetype2") def test_stroke_descender() -> None: # Arrange diff --git a/src/_imagingft.c b/src/_imagingft.c index 5c446e641f5..8bef45baf87 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -833,7 +833,7 @@ font_render(FontObject *self, PyObject *args) { Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */ - int stroke_width = 0; + float stroke_width = 0; PY_LONG_LONG foreground_ink_long = 0; unsigned int foreground_ink; const char *mode = NULL; @@ -853,7 +853,7 @@ font_render(FontObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "OO|zzOzizLffO:render", + "OO|zzOzfzLffO:render", &string, &fill, &mode, @@ -919,8 +919,8 @@ font_render(FontObject *self, PyObject *args) { return NULL; } - width += stroke_width * 2 + ceil(x_start); - height += stroke_width * 2 + ceil(y_start); + width += ceil(stroke_width * 2 + x_start); + height += ceil(stroke_width * 2 + y_start); image = PyObject_CallFunction(fill, "ii", width, height); if (image == Py_None) { PyMem_Del(glyph_info); @@ -934,8 +934,8 @@ font_render(FontObject *self, PyObject *args) { Py_XDECREF(imageId); im = (Imaging)id; - x_offset -= stroke_width; - y_offset -= stroke_width; + x_offset = round(x_offset - stroke_width); + y_offset = round(y_offset - stroke_width); if (count == 0 || width == 0 || height == 0) { PyMem_Del(glyph_info); return Py_BuildValue("N(ii)", image, x_offset, y_offset); @@ -950,7 +950,7 @@ font_render(FontObject *self, PyObject *args) { FT_Stroker_Set( stroker, - (FT_Fixed)stroke_width * 64, + (FT_Fixed)round(stroke_width * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0 @@ -988,8 +988,8 @@ font_render(FontObject *self, PyObject *args) { } /* set pen position to text origin */ - x = (-x_min + stroke_width + x_start) * 64; - y = (-y_max + (-stroke_width) - y_start) * 64; + x = round((-x_min + stroke_width + x_start) * 64); + y = round((-y_max + (-stroke_width) - y_start) * 64); if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; From 4a284a78c58e6952f4adc7230a4349defe38dcae Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 1 Sep 2024 20:40:04 +0400 Subject: [PATCH 40/66] Remove weird comments --- src/encode.c | 7 ------- src/libImaging/Jpeg2K.h | 7 ------- src/libImaging/Jpeg2KDecode.c | 7 ------- src/libImaging/Jpeg2KEncode.c | 7 ------- 4 files changed, 28 deletions(-) diff --git a/src/encode.c b/src/encode.c index 529982dadc5..1a4cd489da2 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1411,10 +1411,3 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } #endif - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index e8d92f7b6bc..6fbbf7311a1 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -104,10 +104,3 @@ typedef struct { int plt; } JPEG2KENCODESTATE; - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index a81a14a09c0..fc927d2f0c0 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -979,10 +979,3 @@ ImagingJpeg2KVersion(void) { } #endif /* HAVE_OPENJPEG */ - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index cb21a186c0b..d30ccde603e 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -652,10 +652,3 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { } #endif /* HAVE_OPENJPEG */ - -/* - * Local Variables: - * c-basic-offset: 4 - * End: - * - */ From 77503156b17a8314652dcd69941cbdb222dd7bcf Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Wed, 11 Sep 2024 14:22:45 +0200 Subject: [PATCH 41/66] Update src/PIL/ImageFont.py Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- 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 55ab6195184..0fccee32ff5 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -932,9 +932,9 @@ def load_path(filename: str | bytes) -> ImageFont: return load(os.path.join(directory, filename)) except OSError: pass - msg = f"cannot find font file '{filename}' in `sys.path`" + msg = f"cannot find font file '{filename}' in sys.path" if os.path.exists(filename): - msg += f', did you mean `ImageFont.load("{filename}")` instead?' + msg += f', did you mean ImageFont.load("{filename}") instead?' raise OSError(msg) From 32c514d24ccac400b025c981bb27b48d6e78b2b6 Mon Sep 17 00:00:00 2001 From: Yngve Mardal Moe Date: Wed, 11 Sep 2024 14:54:53 +0200 Subject: [PATCH 42/66] Update src/PIL/ImageFont.py 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 0fccee32ff5..89efe5bd8cc 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -932,7 +932,7 @@ def load_path(filename: str | bytes) -> ImageFont: return load(os.path.join(directory, filename)) except OSError: pass - msg = f"cannot find font file '{filename}' in sys.path" + msg = f'cannot find font file "{filename}" in sys.path' if os.path.exists(filename): msg += f', did you mean ImageFont.load("{filename}") instead?' From aa7dfe3a56380a4ba98a7b439fbc3085603beae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Sep 2024 16:16:21 +1000 Subject: [PATCH 43/66] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index af292f7a135..f23ec609fa4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Accept float stroke widths #8369 + [radarhere] + - Deprecate ICNS (width, height, scale) sizes in favour of load(scale) #8352 [radarhere] From fab19b0af822128f305f8ffedd0645f33160f5d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Sep 2024 16:57:28 +1000 Subject: [PATCH 44/66] Windows wheels are now grouped with the others --- wheels/README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/wheels/README.md b/wheels/README.md index 8b412b7fe32..1175616e147 100644 --- a/wheels/README.md +++ b/wheels/README.md @@ -1,11 +1,11 @@ README ------ -[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux -wheels for tagged versions of Pillow. +[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build wheels for tagged +versions of Pillow. This directory contains [multibuild](https://github.com/multi-build/multibuild) to -build dependencies for the wheels, and dependency licenses to be included. +build dependencies for macOS and Linux wheels, and dependency licenses to be included. Archives -------- @@ -30,6 +30,3 @@ Wheels Wheels are [GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml). - -Windows wheels are created separately. They are -[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain). From 8336852b610f3a2b83c1d0706c0d76998526c709 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Sep 2024 18:52:07 +1000 Subject: [PATCH 45/66] Install NumPy from PyPI for free threaded job --- .ci/install.sh | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 7e3a691e566..e85e6bdc575 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -38,12 +38,7 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - # TODO Update condition when NumPy supports free-threading - if [[ "$PYTHON_GIL" == "0" ]]; then - python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - else - python3 -m pip install numpy - fi + python3 -m pip install numpy # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then From c9f88890a56882ca43a5e00fb6ed1892f706eaa0 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 12 Sep 2024 14:52:24 +0300 Subject: [PATCH 46/66] Install numpy from PyPI when testing wheels --- .github/workflows/wheels-test.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 023a338244e..eebfd5c55ea 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -15,11 +15,7 @@ fi if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then # TODO Update condition when NumPy supports free-threading - if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then - python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - else - python3 -m pip install numpy - fi + python3 -m pip install numpy fi if [ ! -d "test-images-main" ]; then From 1ead7791c697d5ea76c299156680a3cdd2fa7f14 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Thu, 12 Sep 2024 16:34:29 +0300 Subject: [PATCH 47/66] Remove comment --- .github/workflows/wheels-test.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index eebfd5c55ea..bdf0caffa61 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -14,7 +14,6 @@ else fi if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then - # TODO Update condition when NumPy supports free-threading python3 -m pip install numpy fi From fea929bf10dbf0c77cffd1375a5542d40d24b2f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2024 07:34:56 +1000 Subject: [PATCH 48/66] Test NumPy with musllinux wheels --- .github/workflows/wheels-test.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index bdf0caffa61..b30b1725f94 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -13,9 +13,7 @@ else yum install -y fribidi fi -if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then - python3 -m pip install numpy -fi +python3 -m pip install numpy if [ ! -d "test-images-main" ]; then curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip From 5d8e891387cfd2313d021578b9a049a057980726 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2024 10:52:36 +1000 Subject: [PATCH 49/66] Updated libpng to 1.6.44 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index c25e1a27d94..f87ace4c609 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -21,7 +21,7 @@ if [[ "$MB_ML_VER" != 2014 ]]; then else HARFBUZZ_VERSION=8.5.0 fi -LIBPNG_VERSION=1.6.43 +LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.3 OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.4.5 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 423aaa96607..6d7d5dcdf20 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -115,7 +115,7 @@ def cmd_msbuild( "HARFBUZZ": "9.0.0", "JPEGTURBO": "3.0.3", "LCMS2": "2.16", - "LIBPNG": "1.6.43", + "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", "OPENJPEG": "2.5.2", "TIFF": "4.6.0", From 0f67dfc380a460eb2b4d2bcd43e54b59240006b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2024 15:31:37 +1000 Subject: [PATCH 50/66] Change macos-14 to macos-latest --- .github/workflows/test.yml | 4 ++-- .github/workflows/wheels.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0903f8869c..6576292b5e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: fail-fast: false matrix: os: [ - "macos-14", + "macos-latest", "ubuntu-latest", ] python-version: [ @@ -56,7 +56,7 @@ jobs: # M1 only available for 3.10+ - { os: "macos-13", python-version: "3.9" } exclude: - - { os: "macos-14", python-version: "3.9" } + - { os: "macos-latest", python-version: "3.9" } runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index f3ed8399bd3..e208417e126 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -107,7 +107,7 @@ jobs: cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" - os: macos-14 + os: macos-latest cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" From 1557f7e36f134979ee607e7583185f8b3adc2212 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2024 23:41:03 +1000 Subject: [PATCH 51/66] Removed unused variable --- src/path.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/path.c b/src/path.c index b96e8b78a70..f4f4287f388 100644 --- a/src/path.c +++ b/src/path.c @@ -44,7 +44,6 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); typedef struct { PyObject_HEAD Py_ssize_t count; double *xy; - int index; /* temporary use, e.g. in decimate */ } PyPathObject; static PyTypeObject PyPathType; From 57131599682b1c6a54748006ccda4abfd0f2615e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 23:52:11 +0000 Subject: [PATCH 52/66] Update dependency cibuildwheel to v2.21.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index b8e6c39473e..210f29d3a0c 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.20.0 +cibuildwheel==2.21.0 From a3d9529f045af093eb964bf264064b261714b127 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Sep 2024 11:23:09 +1000 Subject: [PATCH 53/66] Updated macOS deployment target for Python >= 3.12 on Intel to 10.13 --- .github/workflows/wheels.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e208417e126..11564c14256 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -102,10 +102,16 @@ jobs: fail-fast: false matrix: include: - - name: "macOS x86_64" + - name: "macOS 10.10 x86_64" os: macos-13 cibw_arch: x86_64 + build: "pp310* cp3{9,10,11}*" macosx_deployment_target: "10.10" + - name: "macOS 10.13 x86_64" + os: macos-13 + cibw_arch: x86_64 + build: "cp3{12,13}*" + macosx_deployment_target: "10.13" - name: "macOS arm64" os: macos-latest cibw_arch: arm64 @@ -145,7 +151,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl windows: From 10c118bb5c2dca0343d1e6e733de51e519f0374a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Sep 2024 19:58:57 +1000 Subject: [PATCH 54/66] Added release notes for #8379 --- docs/releasenotes/11.0.0.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 5f90348ad46..1b84076361d 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -23,6 +23,13 @@ Python 3.8 Pillow has dropped support for Python 3.8, which reached end-of-life in October 2024. +Python 3.12 on macOS < 10.13 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The latest version of Python 3.12 no longer supports macOS versions earlier than 10.13, +and so Pillow has also updated the deployment target for its prebuilt Python 3.12 +wheels. + PSFile ^^^^^^ @@ -147,6 +154,8 @@ Pillow 11.0.0 now officially supports Python 3.13. Support has also been added for the experimental free-threaded mode of :pep:`703`. +Python 3.13 does not support macOS versions earlier than 10.13. + C-level Flags ^^^^^^^^^^^^^ From ad0091096f4c653c88a98a03e1323d855152bb81 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 14 Sep 2024 20:52:49 +1000 Subject: [PATCH 55/66] Describe supported OS versions instead of unsupported Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/11.0.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 1b84076361d..36334e39f5e 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -23,10 +23,10 @@ Python 3.8 Pillow has dropped support for Python 3.8, which reached end-of-life in October 2024. -Python 3.12 on macOS < 10.13 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Python 3.12 on macOS <= 10.12 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The latest version of Python 3.12 no longer supports macOS versions earlier than 10.13, +The latest version of Python 3.12 only supports macOS versions 10.13 and later, and so Pillow has also updated the deployment target for its prebuilt Python 3.12 wheels. @@ -154,7 +154,7 @@ Pillow 11.0.0 now officially supports Python 3.13. Support has also been added for the experimental free-threaded mode of :pep:`703`. -Python 3.13 does not support macOS versions earlier than 10.13. +Python 3.13 only supports macOS versions 10.13 and later. C-level Flags ^^^^^^^^^^^^^ From 88b3265a9cb86ffd8a7c43c1afa8dae1b78cf035 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Sep 2024 20:59:02 +1000 Subject: [PATCH 56/66] Lint: Run PT016 --- Tests/test_image.py | 15 +++++---------- Tests/test_imageshow.py | 2 +- pyproject.toml | 1 - 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index cf5b5a710cc..9b65041f4b4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1086,22 +1086,17 @@ def test_overrun(self, path: str) -> None: valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c """ with Image.open(os.path.join("Tests/images", path)) as im: - try: + with pytest.raises(OSError) as e: im.load() - pytest.fail() - except OSError as e: - buffer_overrun = str(e) == "buffer overrun when reading image file" - truncated = "image file is truncated" in str(e) + buffer_overrun = str(e.value) == "buffer overrun when reading image file" + truncated = "image file is truncated" in str(e.value) - assert buffer_overrun or truncated + assert buffer_overrun or truncated def test_fli_overrun2(self) -> None: with Image.open("Tests/images/fli_overrun2.bin") as im: - try: + with pytest.raises(OSError, match="buffer overrun when reading image file"): im.seek(1) - pytest.fail() - except OSError as e: - assert str(e) == "buffer overrun when reading image file" def test_exit_fp(self) -> None: with Image.new("L", (1, 1)) as im: diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index a4f7e5cc5b4..67c32783fc5 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -115,7 +115,7 @@ def test_ipythonviewer() -> None: test_viewer = viewer break else: - pytest.fail() + pytest.fail("IPythonViewer not found") im = hopper() assert test_viewer.show(im) == 1 diff --git a/pyproject.toml b/pyproject.toml index 4042bd9ee20..228c344e864 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,6 @@ lint.ignore = [ "PT007", # pytest-parametrize-values-wrong-type "PT011", # pytest-raises-too-broad "PT012", # pytest-raises-with-multiple-statements - "PT016", # pytest-fail-without-message "PT017", # pytest-assert-in-except "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 From c01d2d05772dd52e316ccd0b868a1c7b560cc391 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Sep 2024 21:57:45 +1000 Subject: [PATCH 57/66] Updated libjpeg-turbo to 3.0.4 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index d1e87899fee..4d40f7fabfc 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -22,7 +22,7 @@ else HARFBUZZ_VERSION=8.5.0 fi LIBPNG_VERSION=1.6.44 -JPEGTURBO_VERSION=3.0.3 +JPEGTURBO_VERSION=3.0.4 OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.2 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b478c1c6b7a..e2022d28337 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ def cmd_msbuild( "FREETYPE": "2.13.3", "FRIBIDI": "1.0.15", "HARFBUZZ": "9.0.0", - "JPEGTURBO": "3.0.3", + "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", From 8a086edbe965bff4d89fc3b189e021f0b1a2b322 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Sep 2024 08:22:58 +1000 Subject: [PATCH 58/66] Cast handle to int --- Tests/test_imagewin.py | 12 ++++++++++++ src/PIL/ImageWin.py | 19 +++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index a836bb90b6e..e7af160ddd2 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -60,6 +60,18 @@ def test_dib_mode_string(self) -> None: with pytest.raises(ValueError): ImageWin.Dib(mode) + def test_dib_hwnd(self) -> None: + mode = "RGBA" + size = (128, 128) + wnd = 0 + + dib = ImageWin.Dib(mode, size) + hwnd = ImageWin.HWND(wnd) + + dib.expose(hwnd) + dib.draw(hwnd, (0, 0) + size) + assert isinstance(dib.query_palette(hwnd), int) + def test_dib_paste(self) -> None: # Arrange im = hopper() diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 6fc7cfaf528..98c28f29f1d 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -98,14 +98,15 @@ def expose(self, handle: int | HDC | HWND) -> None: HDC or HWND instance. In PythonWin, you can use ``CDC.GetHandleAttrib()`` to get a suitable handle. """ + handle_int = int(handle) if isinstance(handle, HWND): - dc = self.image.getdc(handle) + dc = self.image.getdc(handle_int) try: self.image.expose(dc) finally: - self.image.releasedc(handle, dc) + self.image.releasedc(handle_int, dc) else: - self.image.expose(handle) + self.image.expose(handle_int) def draw( self, @@ -124,14 +125,15 @@ def draw( """ if src is None: src = (0, 0) + self.size + handle_int = int(handle) if isinstance(handle, HWND): - dc = self.image.getdc(handle) + dc = self.image.getdc(handle_int) try: self.image.draw(dc, dst, src) finally: - self.image.releasedc(handle, dc) + self.image.releasedc(handle_int, dc) else: - self.image.draw(handle, dst, src) + self.image.draw(handle_int, dst, src) def query_palette(self, handle: int | HDC | HWND) -> int: """ @@ -148,14 +150,15 @@ def query_palette(self, handle: int | HDC | HWND) -> int: :return: The number of entries that were changed (if one or more entries, this indicates that the image should be redrawn). """ + handle_int = int(handle) if isinstance(handle, HWND): - handle = self.image.getdc(handle) + handle = self.image.getdc(handle_int) try: result = self.image.query_palette(handle) finally: self.image.releasedc(handle, handle) else: - result = self.image.query_palette(handle) + result = self.image.query_palette(handle_int) return result def paste( From 75e4d5a10d171b9f101f09a671826f3f302139cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 06:12:03 +0000 Subject: [PATCH 59/66] Update dependency cibuildwheel to v2.21.1 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index 210f29d3a0c..7dc3a53fe4d 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.0 +cibuildwheel==2.21.1 From f1e86965f6f791bf1af57f4d202a0c342221190b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 20:26:06 +1000 Subject: [PATCH 60/66] Use transposed size after opening for TIFF images --- Tests/test_image_copy.py | 2 ++ Tests/test_image_resize.py | 4 +--- Tests/test_image_thumbnail.py | 8 +++----- Tests/test_numpy.py | 4 +++- src/PIL/Image.py | 20 +++++--------------- src/PIL/ImageFile.py | 2 +- src/PIL/TiffImagePlugin.py | 16 +++++++++++++++- 7 files changed, 30 insertions(+), 26 deletions(-) diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 027e5338b7a..a0b829cc565 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -49,5 +49,7 @@ def test_copy_zero() -> None: @skip_unless_feature("libtiff") def test_deepcopy() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + out = copy.deepcopy(im) assert out.size == (590, 88) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index c9e3045121f..d9ddf500913 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -300,9 +300,7 @@ def resize(mode: str, size: tuple[int, int] | list[int]) -> None: im.resize((10, 10), "unknown") @skip_unless_feature("libtiff") - def test_load_first(self) -> None: - # load() may change the size of the image - # Test that resize() is calling it before getting the size + def test_transposed(self) -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: im = im.resize((64, 64)) assert im.size == (64, 64) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index bdbf09c407e..01bd4b1d76b 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -92,15 +92,13 @@ def test_no_resize() -> None: @skip_unless_feature("libtiff") -def test_load_first() -> None: - # load() may change the size of the image - # Test that thumbnail() is calling it before performing size calculations +def test_transposed() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + 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) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 312e32e0ca7..040472d69e3 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -238,8 +238,10 @@ def test_zero_size() -> None: @skip_unless_feature("libtiff") -def test_load_first() -> None: +def test_transposed() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: + assert im.size == (590, 88) + a = numpy.array(im) assert a.shape == (88, 590) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f88cdf6e2f7..3f94cef3892 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2332,7 +2332,6 @@ def resize( msg = "reducing_gap must be 1.0 or greater" raise ValueError(msg) - self.load() if box is None: box = (0, 0) + self.size @@ -2781,27 +2780,18 @@ def round_aspect(number: float, key: Callable[[int], float]) -> int: ) return x, y + preserved_size = preserve_aspect_ratio() + if preserved_size is None: + return + final_size = preserved_size + box = None - final_size: tuple[int, int] if reducing_gap is not None: - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size - res = self.draft( None, (int(size[0] * reducing_gap), int(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 - preserved_size = preserve_aspect_ratio() - if preserved_size is None: - return - final_size = preserved_size if self.size != final_size: im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b8bf0b7fe90..721319fd7bf 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -322,7 +322,7 @@ def load(self) -> Image.core.PixelAccess | None: def load_prepare(self) -> None: # create image memory if necessary - if self._im is None or self.im.mode != self.mode or self.im.size != self.size: + if self._im is None or self.im.mode != self.mode: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index cc16cbfb083..28e50474471 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1275,6 +1275,11 @@ def load(self) -> Image.core.PixelAccess | None: return self._load_libtiff() return super().load() + def load_prepare(self) -> None: + if self._im is None and self._will_be_transposed: + self.im = Image.core.new(self.mode, self.size[::-1]) + ImageFile.ImageFile.load_prepare(self) + def load_end(self) -> None: # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. @@ -1416,7 +1421,16 @@ def _setup(self) -> None: if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) - self._size = xsize, ysize + self._will_be_transposed = self.tag_v2.get(ExifTags.Base.Orientation) in ( + 5, + 6, + 7, + 8, + ) + if self._will_be_transposed: + self._size = ysize, xsize + else: + self._size = xsize, ysize logger.debug("- size: %s", self.size) From 84e275d90629bfdf508f81a1142eddd5d5ea46e4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 20:27:35 +1000 Subject: [PATCH 61/66] Loading does not change mode --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 721319fd7bf..d69d8456850 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -322,7 +322,7 @@ def load(self) -> Image.core.PixelAccess | None: def load_prepare(self) -> None: # create image memory if necessary - if self._im is None or self.im.mode != self.mode: + if self._im is None: self.im = Image.core.new(self.mode, self.size) # create palette (optional) if self.mode == "P": From 9135fd0fb24607510f38b5fa4b1b4d279974363a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 20:56:55 +1000 Subject: [PATCH 62/66] Mention limit in error message --- src/PIL/PngImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c268d7b1a28..28ade293e96 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -140,7 +140,7 @@ def _safe_zlib_decompress(s: bytes) -> bytes: dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: - msg = "Decompressed Data Too Large" + msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK" raise ValueError(msg) return plaintext From a859695d9a0bf9d9dc20ef7c053909658a12df6a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Sep 2024 21:17:49 +1000 Subject: [PATCH 63/66] Rearranged code Co-authored-by: Alexander Karpinsky --- src/PIL/TiffImagePlugin.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 28e50474471..437f7d98bd7 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1421,12 +1421,8 @@ def _setup(self) -> None: if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) - self._will_be_transposed = self.tag_v2.get(ExifTags.Base.Orientation) in ( - 5, - 6, - 7, - 8, - ) + orientation = self.tag_v2.get(ExifTags.Base.Orientation) + self._will_be_transposed = orientation in (5, 6, 7, 8) if self._will_be_transposed: self._size = ysize, xsize else: From a92dca66bdeb26106447079fcf2d36bc397298c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 21:22:59 +1000 Subject: [PATCH 64/66] Use raw size for striped tiles --- src/PIL/TiffImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 437f7d98bd7..c024e57fce4 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1569,7 +1569,7 @@ def _setup(self) -> None: if STRIPOFFSETS in self.tag_v2: offsets = self.tag_v2[STRIPOFFSETS] h = self.tag_v2.get(ROWSPERSTRIP, ysize) - w = self.size[0] + w = xsize else: # tiled image offsets = self.tag_v2[TILEOFFSETS] @@ -1603,9 +1603,9 @@ def _setup(self) -> None: ) ) x = x + w - if x >= self.size[0]: + if x >= xsize: x, y = 0, y + h - if y >= self.size[1]: + if y >= ysize: x = y = 0 layer += 1 else: From 629f5be52eb9043d70b5d5467f10a07ad3a68cdf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Sep 2024 22:56:29 +1000 Subject: [PATCH 65/66] In seek(), create core image at size needed for loading --- src/PIL/TiffImagePlugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index c024e57fce4..14e6ea2786e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1194,8 +1194,8 @@ def seek(self, frame: int) -> None: # Create a new core image object on second and # subsequent frames in the image. Image may be # different size/mode. - Image._decompression_bomb_check(self.size) - self.im = Image.core.new(self.mode, self.size) + Image._decompression_bomb_check(self._tile_size) + self.im = Image.core.new(self.mode, self._tile_size) def _seek(self, frame: int) -> None: self.fp = self._fp @@ -1276,8 +1276,8 @@ def load(self) -> Image.core.PixelAccess | None: return super().load() def load_prepare(self) -> None: - if self._im is None and self._will_be_transposed: - self.im = Image.core.new(self.mode, self.size[::-1]) + if self._im is None: + self.im = Image.core.new(self.mode, self._tile_size) ImageFile.ImageFile.load_prepare(self) def load_end(self) -> None: @@ -1421,9 +1421,9 @@ def _setup(self) -> None: if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) + self._tile_size = xsize, ysize orientation = self.tag_v2.get(ExifTags.Base.Orientation) - self._will_be_transposed = orientation in (5, 6, 7, 8) - if self._will_be_transposed: + if orientation in (5, 6, 7, 8): self._size = ysize, xsize else: self._size = xsize, ysize From 9adb476f37f28cd4a27bb66c6242e4d11925b53b Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Sep 2024 23:58:23 +1000 Subject: [PATCH 66/66] Rearranged text --- src/PIL/ImageFont.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 70cd57de7be..b694b817e65 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -808,10 +808,10 @@ def truetype( ) -> FreeTypeFont: """ Load a TrueType or OpenType font from a file or file-like object, - and create a font object. For loading bitmap fonts instead, - see :py:func:`~PIL.ImageFont.load` and :py:func:`~PIL.ImageFont.load_path`. - 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. + and create a font object. 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. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load` + and :py:func:`~PIL.ImageFont.load_path`. 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