From d23d56e195d34734b42452995682e4a9649e5332 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 17 Jun 2025 23:10:15 +1000 Subject: [PATCH 1/8] Deprecate saving I mode images as PNG --- Tests/test_file_png.py | 14 ++++++++++++-- docs/deprecations.rst | 14 ++++++++++++++ docs/releasenotes/11.3.0.rst | 13 ++++++++++--- src/PIL/PngImagePlugin.py | 3 +++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0f0886ab8..15f67385a 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -100,11 +100,11 @@ class TestFilePng: assert im.format == "PNG" assert im.get_format_mimetype() == "image/png" - for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: + for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]: im = hopper(mode) im.save(test_file) with Image.open(test_file) as reloaded: - if mode in ("I", "I;16B"): + if mode == "I;16B": reloaded = reloaded.convert(mode) assert_image_equal(reloaded, im) @@ -801,6 +801,16 @@ class TestFilePng: with Image.open("Tests/images/truncated_end_chunk.png") as im: assert_image_equal_tofile(im, "Tests/images/hopper.png") + def test_deprecation(self, tmp_path: Path) -> None: + test_file = tmp_path / "out.png" + + im = hopper("I") + with pytest.warns(DeprecationWarning): + im.save(test_file) + + with Image.open(test_file) as reloaded: + assert_image_equal(im, reloaded.convert("I")) + @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0490ba439..a36eb4aa7 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -193,6 +193,20 @@ Image.Image.get_child_images() method uses an image's file pointer, and so child images could only be retrieved from an :py:class:`PIL.ImageFile.ImageFile` instance. +Saving I mode images as PNG +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.3.0 + +In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain +at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly +changing the data, this is now deprecated. Instead, the image can be converted to +another mode before saving:: + + from PIL import Image + im = Image.new("I", (1, 1)) + im.convert("I;16").save("out.png") + Removed features ---------------- diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index ba091fa2c..5dd151bf3 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -23,10 +23,17 @@ TODO Deprecations ============ -TODO -^^^^ +Saving I mode images as PNG +^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain +at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly +changing the data, this is now deprecated. Instead, the image can be converted to +another mode before saving:: + + from PIL import Image + im = Image.new("I", (1, 1)) + im.convert("I;16").save("out.png") API changes =========== diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index f3815a122..7999381a6 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -48,6 +48,7 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +from ._deprecate import deprecate from ._util import DeferredError TYPE_CHECKING = False @@ -1368,6 +1369,8 @@ def _save( except KeyError as e: msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e + if outmode == "I": + deprecate("Saving I mode images as PNG", 13) # # write minimal PNG file From 79e0b0b6ada86f16bf7a8cf1c878756225d85d44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Jun 2025 22:19:20 +1000 Subject: [PATCH 2/8] Allow for custom stacklevel in deprecations --- src/PIL/PngImagePlugin.py | 2 +- src/PIL/_deprecate.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 7999381a6..1b9a89aef 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1370,7 +1370,7 @@ def _save( msg = f"cannot write mode {mode} as PNG" raise OSError(msg) from e if outmode == "I": - deprecate("Saving I mode images as PNG", 13) + deprecate("Saving I mode images as PNG", 13, stacklevel=4) # # write minimal PNG file diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 9f9d8bbc9..170d44490 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -12,6 +12,7 @@ def deprecate( *, action: str | None = None, plural: bool = False, + stacklevel: int = 3, ) -> None: """ Deprecations helper. @@ -67,5 +68,5 @@ def deprecate( warnings.warn( f"{deprecated} {is_} deprecated and will be removed in {removed}{action}", DeprecationWarning, - stacklevel=3, + stacklevel=stacklevel, ) From 216dc4ca60947b4076defa2f0565f8b74a7864e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Jun 2025 19:12:23 +1000 Subject: [PATCH 3/8] Added Python 3.14 macOS x86-64 wheels --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 72516651f..229e23ef0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -58,7 +58,7 @@ jobs: - name: "macOS 10.13 x86_64" os: macos-13 cibw_arch: x86_64 - build: "cp3{12,13}*" + build: "cp3{12,13,14}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" os: macos-13 From ae025183148a900511e7f8866ca5c28f3efc7b5f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 22 Jun 2025 22:08:51 +1000 Subject: [PATCH 4/8] Use same AVIF URL when fetching dependency (#8871) --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 098716b60..4baa5ccef 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -385,8 +385,8 @@ DEPS: dict[str, dict[str, Any]] = { "bins": [r"*.dll"], }, "libavif": { - "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", - "filename": f"libavif-{V['LIBAVIF']}.zip", + "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.tar.gz", + "filename": f"libavif-{V['LIBAVIF']}.tar.gz", "license": "LICENSE", "build": [ "rustup update", From 2954964cd2b70c463d75bd7f828c7bb7f3802bf2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 23 Jun 2025 07:05:43 +1000 Subject: [PATCH 5/8] Removed ImageCmsProfile._set method (#9032) Co-authored-by: Luke Granger-Brown --- src/PIL/ImageCms.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index fdfbee789..a1584f111 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -248,6 +248,9 @@ class ImageCmsProfile: low-level profile object """ + self.filename = None + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info if isinstance(profile, str): if sys.platform == "win32": @@ -256,23 +259,18 @@ class ImageCmsProfile: profile_bytes_path.decode("ascii") except UnicodeDecodeError: with open(profile, "rb") as f: - self._set(core.profile_frombytes(f.read())) + self.profile = core.profile_frombytes(f.read()) return - self._set(core.profile_open(profile), profile) + self.filename = profile + self.profile = core.profile_open(profile) elif hasattr(profile, "read"): - self._set(core.profile_frombytes(profile.read())) + self.profile = core.profile_frombytes(profile.read()) elif isinstance(profile, core.CmsProfile): - self._set(profile) + self.profile = profile else: msg = "Invalid type for Profile" # type: ignore[unreachable] raise TypeError(msg) - def _set(self, profile: core.CmsProfile, filename: str | None = None) -> None: - self.profile = profile - self.filename = filename - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info - def tobytes(self) -> bytes: """ Returns the profile in a format suitable for embedding in From 1557585411f1f891180775dab40cfeec236d69bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Jun 2025 20:29:38 +1000 Subject: [PATCH 6/8] Use percent formatting --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3716a7b9f..256c78fc9 100644 --- a/setup.py +++ b/setup.py @@ -509,11 +509,11 @@ class pil_build_ext(build_ext): if root is None and pkg_config: if isinstance(lib_name, str): - _dbg(f"Looking for `{lib_name}` using pkg-config.") + _dbg("Looking for `%s` using pkg-config.", lib_name) root = pkg_config(lib_name) else: for lib_name2 in lib_name: - _dbg(f"Looking for `{lib_name2}` using pkg-config.") + _dbg("Looking for `%s` using pkg-config.", lib_name2) root = pkg_config(lib_name2) if root: break From 18f8af78d3f56639f91f4b788ab9d89ae573b72c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Jun 2025 20:35:09 +1000 Subject: [PATCH 7/8] Pass strings or tuples of strings to _dbg --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 256c78fc9..f05379d6d 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,6 @@ import subprocess import sys import warnings from collections.abc import Iterator -from typing import Any from setuptools import Extension, setup from setuptools.command.build_ext import build_ext @@ -148,7 +147,7 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -def _dbg(s: str, tp: Any = None) -> None: +def _dbg(s: str, tp: str | tuple[str, ...] | None = None) -> None: if DEBUG: if tp: print(s % tp) @@ -732,7 +731,7 @@ class pil_build_ext(build_ext): best_path = os.path.join(directory, name) _dbg( "Best openjpeg version %s so far in %s", - (best_version, best_path), + (str(best_version), best_path), ) if best_version and _find_library_file(self, "openjp2"): From acd8b0c2acfa1c69f91a14d5881a7f9bf436900f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:09:31 +1000 Subject: [PATCH 8/8] Fix libtiff cleanup (#9002) --- src/libImaging/TiffDecode.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 2e83fb847..e289ce405 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -1032,7 +1032,10 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - if (!clientstate->fp) { + if (clientstate->fp) { + TIFFCleanup(tiff); + clientstate->tiff = NULL; + } else { free(clientstate->data); } return -1;