From 2059e060051acd2024360834da435a266d9dc665 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 09:56:47 +0100 Subject: [PATCH 01/89] Add parallel compile from pybind11 --- pyproject.toml | 1 + setup.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 683ab24ef..ae4b70990 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] build-backend = "backend" requires = [ + "pybind11", "setuptools>=77", ] backend-path = [ diff --git a/setup.py b/setup.py index ab36c6b17..ec6b47b1c 100644 --- a/setup.py +++ b/setup.py @@ -18,9 +18,12 @@ import warnings from collections.abc import Iterator from typing import Any +from pybind11.setup_helpers import ParallelCompile from setuptools import Extension, setup from setuptools.command.build_ext import build_ext +ParallelCompile("MAX_CONCURRENCY", default=0).install() + def get_version() -> str: version_file = "src/PIL/_version.py" @@ -1048,12 +1051,12 @@ ext_modules = [ Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] - # parse configuration from _custom_build/backend.py while sys.argv[-1].startswith("--pillow-configuration="): _, key, value = sys.argv.pop().split("=", 2) configuration.setdefault(key, []).append(value) + try: setup( cmdclass={"build_ext": pil_build_ext}, From b931402046f840bedc09b3c2b0c4039ac28531dc Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Sat, 31 May 2025 15:14:17 +0200 Subject: [PATCH 02/89] add pybind11 elsewhere so mypy can find it --- .ci/requirements-mypy.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 86ac2e0b2..645605aa6 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -5,6 +5,7 @@ ipython numpy packaging pyarrow-stubs +pybind11 pytest sphinx types-atheris From 2316c930f9b2985d27894f043f5f2e4787543dca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Jun 2025 22:46:09 +1000 Subject: [PATCH 03/89] Removed default argument --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ec6b47b1c..233811192 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ from pybind11.setup_helpers import ParallelCompile from setuptools import Extension, setup from setuptools.command.build_ext import build_ext -ParallelCompile("MAX_CONCURRENCY", default=0).install() +ParallelCompile("MAX_CONCURRENCY").install() def get_version() -> str: @@ -1051,12 +1051,12 @@ ext_modules = [ Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), ] + # parse configuration from _custom_build/backend.py while sys.argv[-1].startswith("--pillow-configuration="): _, key, value = sys.argv.pop().split("=", 2) configuration.setdefault(key, []).append(value) - try: setup( cmdclass={"build_ext": pil_build_ext}, From ecd264fffc680ab05da6a71ff4466c774185ab90 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Jun 2025 22:22:46 +1000 Subject: [PATCH 04/89] Use "parallel" config setting and 4 as defaults --- setup.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 1134879be..6c2180ebd 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,15 @@ from pybind11.setup_helpers import ParallelCompile from setuptools import Extension, setup from setuptools.command.build_ext import build_ext -ParallelCompile("MAX_CONCURRENCY").install() +configuration: dict[str, list[str]] = {} + +# parse configuration from _custom_build/backend.py +while sys.argv[-1].startswith("--pillow-configuration="): + _, key, value = sys.argv.pop().split("=", 2) + configuration.setdefault(key, []).append(value) + +default = int(configuration.get("parallel", ["4"])[-1]) +ParallelCompile("MAX_CONCURRENCY", default).install() def get_version() -> str: @@ -30,9 +38,6 @@ def get_version() -> str: return f.read().split('"')[1] -configuration: dict[str, list[str]] = {} - - PILLOW_VERSION = get_version() AVIF_ROOT = None FREETYPE_ROOT = None @@ -1047,11 +1052,6 @@ ext_modules = [ ] -# parse configuration from _custom_build/backend.py -while sys.argv[-1].startswith("--pillow-configuration="): - _, key, value = sys.argv.pop().split("=", 2) - configuration.setdefault(key, []).append(value) - try: setup( cmdclass={"build_ext": pil_build_ext}, From 23ed906b622e25466981fa7c2b80f2a1da612661 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Jun 2025 22:00:36 +1000 Subject: [PATCH 05/89] Removed default limit of 4 --- docs/installation/building-from-source.rst | 5 ++--- setup.py | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 8988a92ce..4c114a5e2 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -276,10 +276,9 @@ Build options * Config setting: ``-C parallel=n``. Can also be given with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use - multiprocessing to build the extension. Setting ``-C parallel=n`` + multiprocessing to build the extensions. Setting ``-C parallel=n`` sets the number of CPUs to use to ``n``, or can disable parallel building by - using a setting of 1. By default, it uses 4 CPUs, or if 4 are not - available, as many as are present. + using a setting of 1. By default, it uses as many CPUs as are present. * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, diff --git a/setup.py b/setup.py index 6c2180ebd..aee1b04eb 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ while sys.argv[-1].startswith("--pillow-configuration="): _, key, value = sys.argv.pop().split("=", 2) configuration.setdefault(key, []).append(value) -default = int(configuration.get("parallel", ["4"])[-1]) +default = int(configuration.get("parallel", ["0"])[-1]) ParallelCompile("MAX_CONCURRENCY", default).install() @@ -394,9 +394,7 @@ class pil_build_ext(build_ext): cpu_count = os.cpu_count() if cpu_count is not None: try: - self.parallel = int( - os.environ.get("MAX_CONCURRENCY", min(4, cpu_count)) - ) + self.parallel = int(os.environ.get("MAX_CONCURRENCY", cpu_count)) except TypeError: pass for x in self.feature: From 5554e778bba52f2414d0151642a2906aed254ad2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Jul 2025 13:18:48 +1000 Subject: [PATCH 06/89] Removed unnecessary checks --- src/libImaging/AlphaComposite.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index 6d728f908..e14af0dea 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -25,13 +25,11 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { int x, y; /* Check arguments */ - if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA") || - imDst->type != IMAGING_TYPE_UINT8 || imDst->bands != 4) { + if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA")) { return ImagingError_ModeError(); } - if (strcmp(imDst->mode, imSrc->mode) || imDst->type != imSrc->type || - imDst->bands != imSrc->bands || imDst->xsize != imSrc->xsize || + if (strcmp(imDst->mode, imSrc->mode) || imDst->xsize != imSrc->xsize || imDst->ysize != imSrc->ysize) { return ImagingError_Mismatch(); } From 3152da47355ed3f9b342dc94de8a2214798842c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Jul 2025 13:51:18 +1000 Subject: [PATCH 07/89] Allow alpha_composite to use LA images --- Tests/test_image.py | 31 +++++++++++++++++++++++++++++++ src/PIL/Image.py | 5 ++--- src/libImaging/AlphaComposite.c | 3 ++- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 6b8b6d42b..e4c25693a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -398,6 +398,37 @@ class TestImage: assert img_colors is not None assert sorted(img_colors) == expected_colors + def test_alpha_composite_la(self) -> None: + # Arrange + expected_colors = sorted( + [ + (3300, (255, 255)), + (1156, (170, 192)), + (1122, (128, 255)), + (1089, (0, 0)), + (1122, (255, 128)), + (1122, (0, 128)), + (1089, (0, 255)), + ] + ) + + dst = Image.new("LA", size=(100, 100), color=(0, 255)) + draw = ImageDraw.Draw(dst) + draw.rectangle((0, 33, 100, 66), fill=(0, 128)) + draw.rectangle((0, 67, 100, 100), fill=(0, 0)) + src = Image.new("LA", size=(100, 100), color=(255, 255)) + draw = ImageDraw.Draw(src) + draw.rectangle((33, 0, 66, 100), fill=(255, 128)) + draw.rectangle((67, 0, 100, 100), fill=(255, 0)) + + # Act + img = Image.alpha_composite(dst, src) + + # Assert + img_colors = img.getcolors() + assert img_colors is not None + assert sorted(img_colors) == expected_colors + def test_alpha_inplace(self) -> None: src = Image.new("RGBA", (128, 128), "blue") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d209405c4..f4f1eea79 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3588,9 +3588,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image: """ Alpha composite im2 over im1. - :param im1: The first image. Must have mode RGBA. - :param im2: The second image. Must have mode RGBA, and the same size as - the first image. + :param im1: The first image. Must have mode RGBA or LA. + :param im2: The second image. Must have the same mode and size as the first image. :returns: An :py:class:`~PIL.Image.Image` object. """ diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index e14af0dea..44c451679 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -25,7 +25,8 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { int x, y; /* Check arguments */ - if (!imDst || !imSrc || strcmp(imDst->mode, "RGBA")) { + if (!imDst || !imSrc || + (strcmp(imDst->mode, "RGBA") && strcmp(imDst->mode, "LA"))) { return ImagingError_ModeError(); } From dc7d646db03bb34abd493a79ec2ceb78ec778265 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 22:52:23 +1000 Subject: [PATCH 08/89] Use correct bands for 2 band histograms --- Tests/test_image_histogram.py | 3 +++ src/libImaging/Histo.c | 14 +++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index dbd55d4c2..436eb78a2 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -10,9 +10,12 @@ def test_histogram() -> None: assert histogram("1") == (256, 0, 10994) assert histogram("L") == (256, 0, 662) + assert histogram("LA") == (512, 0, 16384) + assert histogram("La") == (512, 0, 16384) assert histogram("I") == (256, 0, 662) assert histogram("F") == (256, 0, 662) assert histogram("P") == (256, 0, 1551) + assert histogram("PA") == (512, 0, 16384) assert histogram("RGB") == (768, 4, 675) assert histogram("RGBA") == (1024, 0, 16384) assert histogram("CMYK") == (1024, 0, 16384) diff --git a/src/libImaging/Histo.c b/src/libImaging/Histo.c index c5a547a64..87c09d3d4 100644 --- a/src/libImaging/Histo.c +++ b/src/libImaging/Histo.c @@ -132,11 +132,15 @@ ImagingGetHistogram(Imaging im, Imaging imMask, void *minmax) { ImagingSectionEnter(&cookie); for (y = 0; y < im->ysize; y++) { UINT8 *in = (UINT8 *)im->image[y]; - for (x = 0; x < im->xsize; x++) { - h->histogram[(*in++)]++; - h->histogram[(*in++) + 256]++; - h->histogram[(*in++) + 512]++; - h->histogram[(*in++) + 768]++; + for (x = 0; x < im->xsize; x++, in += 4) { + h->histogram[*in]++; + if (im->bands == 2) { + h->histogram[*(in + 3) + 256]++; + } else { + h->histogram[*(in + 1) + 256]++; + h->histogram[*(in + 2) + 512]++; + h->histogram[*(in + 3) + 768]++; + } } } ImagingSectionLeave(&cookie); From 99737228c5a65d5291ecdf4d9718a34a815e8b32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Jul 2025 06:53:22 +1000 Subject: [PATCH 09/89] Only deprecate fromarray mode for changing data types --- Tests/test_image_array.py | 16 +++++--- src/PIL/Image.py | 79 +++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 46 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index ecbce3d6f..abb22f949 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -101,9 +101,8 @@ def test_fromarray_strides_without_tobytes() -> None: self.__array_interface__ = arr_params with pytest.raises(ValueError): - wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) - with pytest.warns(DeprecationWarning, match="'mode' parameter"): - Image.fromarray(wrapped, "L") + wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1), "typestr": "|u1"}) + Image.fromarray(wrapped, "L") def test_fromarray_palette() -> None: @@ -112,9 +111,16 @@ def test_fromarray_palette() -> None: a = numpy.array(i) # Act - with pytest.warns(DeprecationWarning, match="'mode' parameter"): - out = Image.fromarray(a, "P") + out = Image.fromarray(a, "P") # Assert that the Python and C palettes match assert out.palette is not None assert len(out.palette.colors) == len(out.im.getpalette()) / 3 + + +def test_deprecation() -> None: + a = numpy.array(im.convert("L")) + with pytest.warns( + DeprecationWarning, match="'mode' parameter for changing data types" + ): + Image.fromarray(a, "1") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 59168f5e3..e512da9a1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3251,19 +3251,9 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: transferred. This means that P and PA mode images will lose their palette. :param obj: Object with array interface - :param mode: Optional mode to use when reading ``obj``. Will be determined from - type if ``None``. Deprecated. - - This will not be used to convert the data after reading, but will be used to - change how the data is read:: - - from PIL import Image - import numpy as np - a = np.full((1, 1), 300) - im = Image.fromarray(a, mode="L") - im.getpixel((0, 0)) # 44 - im = Image.fromarray(a, mode="RGB") - im.getpixel((0, 0)) # (44, 1, 0) + :param mode: Optional mode to use when reading ``obj``. Since pixel values do not + contain information about palettes or color spaces, this can be used to place + grayscale L mode data within a P mode image, or read RGB data as YCbCr. See: :ref:`concept-modes` for general information about modes. :returns: An image object. @@ -3274,21 +3264,28 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: shape = arr["shape"] ndim = len(shape) strides = arr.get("strides", None) - if mode is None: - try: - typekey = (1, 1) + shape[2:], arr["typestr"] - except KeyError as e: + try: + typekey = (1, 1) + shape[2:], arr["typestr"] + except KeyError as e: + if mode is not None: + typekey = None + color_modes: list[str] = [] + else: msg = "Cannot handle this data type" raise TypeError(msg) from e + if typekey is not None: try: - mode, rawmode = _fromarray_typemap[typekey] + typemode, rawmode, color_modes = _fromarray_typemap[typekey] except KeyError as e: typekey_shape, typestr = typekey msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" raise TypeError(msg) from e - else: - deprecate("'mode' parameter", 13) + if mode is not None: + if mode != typemode and mode not in color_modes: + deprecate("'mode' parameter for changing data types", 13) rawmode = mode + else: + mode = typemode if mode in ["1", "L", "I", "P", "F"]: ndmax = 2 elif mode == "RGB": @@ -3385,29 +3382,29 @@ def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: _fromarray_typemap = { - # (shape, typestr) => mode, rawmode + # (shape, typestr) => mode, rawmode, color modes # first two members of shape are set to one - ((1, 1), "|b1"): ("1", "1;8"), - ((1, 1), "|u1"): ("L", "L"), - ((1, 1), "|i1"): ("I", "I;8"), - ((1, 1), "u2"): ("I", "I;16B"), - ((1, 1), "i2"): ("I", "I;16BS"), - ((1, 1), "u4"): ("I", "I;32B"), - ((1, 1), "i4"): ("I", "I;32BS"), - ((1, 1), "f4"): ("F", "F;32BF"), - ((1, 1), "f8"): ("F", "F;64BF"), - ((1, 1, 2), "|u1"): ("LA", "LA"), - ((1, 1, 3), "|u1"): ("RGB", "RGB"), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA"), + ((1, 1), "|b1"): ("1", "1;8", []), + ((1, 1), "|u1"): ("L", "L", ["P"]), + ((1, 1), "|i1"): ("I", "I;8", []), + ((1, 1), "u2"): ("I", "I;16B", []), + ((1, 1), "i2"): ("I", "I;16BS", []), + ((1, 1), "u4"): ("I", "I;32B", []), + ((1, 1), "i4"): ("I", "I;32BS", []), + ((1, 1), "f4"): ("F", "F;32BF", []), + ((1, 1), "f8"): ("F", "F;64BF", []), + ((1, 1, 2), "|u1"): ("LA", "LA", ["La", "PA"]), + ((1, 1, 3), "|u1"): ("RGB", "RGB", ["YCbCr", "LAB", "HSV"]), + ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa"]), # shortcuts: - ((1, 1), f"{_ENDIAN}i4"): ("I", "I"), - ((1, 1), f"{_ENDIAN}f4"): ("F", "F"), + ((1, 1), f"{_ENDIAN}i4"): ("I", "I", []), + ((1, 1), f"{_ENDIAN}f4"): ("F", "F", []), } From 31e6c716ac0141ca03aed750b8b326183a45b0fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Jul 2025 22:26:25 +1000 Subject: [PATCH 10/89] Improved features test coverage --- Tests/test_features.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Tests/test_features.py b/Tests/test_features.py index 520c25b46..ddca99344 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -112,6 +112,25 @@ def test_unsupported_module() -> None: features.version_module(module) +def test_unsupported_feature() -> None: + # Arrange + feature = "unsupported_feature" + # Act / Assert + with pytest.raises(ValueError): + features.check_feature(feature) + with pytest.raises(ValueError): + features.version_feature(feature) + + +def test_unsupported_version() -> None: + assert features.version("unsupported_version") is None + + +def test_modulenotfound(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(features, "features", {"test": ("PIL._test", "", "")}) + assert features.check_feature("test") is None + + @pytest.mark.parametrize("supported_formats", (True, False)) def test_pilinfo(supported_formats: bool) -> None: buf = io.StringIO() From 8b695cc0d36363cd853bd7d0cec7be2e31004537 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 10 Jul 2025 22:50:05 +1000 Subject: [PATCH 11/89] When deleting EXIF IFD tag, clear IFD data --- Tests/test_image.py | 11 +++++++++++ src/PIL/Image.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index 83b027aa2..e6f21c976 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -922,6 +922,17 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) + def test_delete_ifd_tag(self) -> None: + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + exif.get_ifd(0x8769) + assert 0x8769 in exif + del exif[0x8769] + + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert 0x8769 not in reloaded_exif + def test_exif_load_from_fp(self) -> None: with Image.open("Tests/images/flower.jpg") as im: data = im.info["exif"] diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 262b5478b..8901b3034 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -4215,6 +4215,8 @@ class Exif(_ExifBase): del self._info[tag] else: del self._data[tag] + if tag in self._ifds: + del self._ifds[tag] def __iter__(self) -> Iterator[int]: keys = set(self._data) From 74e36e0ee5da824132595c2c13dc1fc8416c743f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 16:48:46 +1000 Subject: [PATCH 12/89] Added RGBX and CMYK as alternatives for RGBA array data --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e512da9a1..c98630cc2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3401,7 +3401,7 @@ _fromarray_typemap = { ((1, 1), ">f8"): ("F", "F;64BF", []), ((1, 1, 2), "|u1"): ("LA", "LA", ["La", "PA"]), ((1, 1, 3), "|u1"): ("RGB", "RGB", ["YCbCr", "LAB", "HSV"]), - ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa"]), + ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa", "RGBX", "CMYK"]), # shortcuts: ((1, 1), f"{_ENDIAN}i4"): ("I", "I", []), ((1, 1), f"{_ENDIAN}f4"): ("F", "F", []), From 561ae3760c8a240d825598c0bd3b0365991586cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 17:18:47 +1000 Subject: [PATCH 13/89] Set correct size for rotated images after opening --- Tests/test_file_pcd.py | 15 +++++++++++++++ src/PIL/PcdImagePlugin.py | 3 +-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 81a316fc1..9bf1a75f0 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,10 +1,15 @@ from __future__ import annotations +from io import BytesIO + +import pytest + from PIL import Image def test_load_raw() -> None: with Image.open("Tests/images/hopper.pcd") as im: + assert im.size == (768, 512) im.load() # should not segfault. # Note that this image was created with a resized hopper @@ -15,3 +20,13 @@ def test_load_raw() -> None: # target = hopper().resize((768,512)) # assert_image_similar(im, target, 10) + + +@pytest.mark.parametrize("orientation", (1, 3)) +def test_rotated(orientation: int) -> None: + with open("Tests/images/hopper.pcd", "rb") as fp: + data = bytearray(fp.read()) + data[2048 + 1538] = orientation + f = BytesIO(data) + with Image.open(f) as im: + assert im.size == (512, 768) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 3aa249988..ac53f616e 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -46,14 +46,13 @@ class PcdImageFile(ImageFile.ImageFile): self.tile_post_rotate = -90 self._mode = "RGB" - self._size = 768, 512 # FIXME: not correct for rotated images! + self._size = (512, 768) if orientation in (1, 3) else (768, 512) self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)] def load_end(self) -> None: if self.tile_post_rotate: # Handle rotated PCDs self.im = self.im.rotate(self.tile_post_rotate) - self._size = self.im.size # From 7328cf2e5e9da9bbf2f2b20859c09707de8b1e4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 17:19:56 +1000 Subject: [PATCH 14/89] Reduced number of bytes read --- src/PIL/PcdImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index ac53f616e..7f9ab525c 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -32,7 +32,7 @@ class PcdImageFile(ImageFile.ImageFile): assert self.fp is not None self.fp.seek(2048) - s = self.fp.read(2048) + s = self.fp.read(1539) if not s.startswith(b"PCD_"): msg = "not a PCD file" From bc2519abf10e6a2a095be82d7a268870afaaba71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 22:45:22 +1000 Subject: [PATCH 15/89] Removed helper method _i8, unused since dump() was removed --- src/PIL/IptcImagePlugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index b1fbb1bf1..e5a52aa8f 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -34,10 +34,6 @@ def _i(c: bytes) -> int: return i32((b"\0\0\0\0" + c)[-4:]) -def _i8(c: int | bytes) -> int: - return c if isinstance(c, int) else c[0] - - ## # Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields # from TIFF and JPEG files, use the getiptcinfo function. From 68ac3375c68a3798d9f964a2ec704c0465ea4566 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jul 2025 12:47:54 +1000 Subject: [PATCH 16/89] Codec is always "iptc" --- src/PIL/IptcImagePlugin.py | 48 ++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index e5a52aa8f..85a13fe2c 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -124,35 +124,33 @@ class IptcImageFile(ImageFile.ImageFile): ] def load(self) -> Image.core.PixelAccess | None: - if len(self.tile) != 1 or self.tile[0][0] != "iptc": - return ImageFile.ImageFile.load(self) + if self.tile: + offset, compression = self.tile[0][2:] - offset, compression = self.tile[0][2:] + self.fp.seek(offset) - self.fp.seek(offset) - - # Copy image data to temporary file - o = BytesIO() - if compression == "raw": - # To simplify access to the extracted file, - # prepend a PPM header - o.write(b"P5\n%d %d\n255\n" % self.size) - while True: - type, size = self.field() - if type != (8, 10): - break - while size > 0: - s = self.fp.read(min(size, 8192)) - if not s: + # Copy image data to temporary file + o = BytesIO() + if compression == "raw": + # To simplify access to the extracted file, + # prepend a PPM header + o.write(b"P5\n%d %d\n255\n" % self.size) + while True: + type, size = self.field() + if type != (8, 10): break - o.write(s) - size -= len(s) + while size > 0: + s = self.fp.read(min(size, 8192)) + if not s: + break + o.write(s) + size -= len(s) - with Image.open(o) as _im: - _im.load() - self.im = _im.im - self.tile = [] - return Image.Image.load(self) + with Image.open(o) as _im: + _im.load() + self.im = _im.im + self.tile = [] + return ImageFile.ImageFile.load(self) Image.register_open(IptcImageFile.format, IptcImageFile) From cfa51ad4ada953c1a32d4cf9e1504de1cfec40b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jul 2025 15:09:07 +1000 Subject: [PATCH 17/89] Populate single band --- Tests/test_file_iptc.py | 69 +++++++++++++++++++++++++++++++++++--- src/PIL/IptcImagePlugin.py | 33 +++++++++++------- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 3c4c892c8..5a8aaa3ef 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -2,6 +2,8 @@ from __future__ import annotations from io import BytesIO +import pytest + from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags from .helper import assert_image_equal, hopper @@ -9,21 +11,78 @@ from .helper import assert_image_equal, hopper TEST_FILE = "Tests/images/iptc.jpg" +def create_iptc_image(info: dict[str, int] = {}) -> BytesIO: + def field(tag, value): + return bytes((0x1C,) + tag + (0, len(value))) + value + + data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0)))) + data += field((3, 120), bytes((info.get("compression", 1),))) + if "band" in info: + data += field((3, 65), bytes((info["band"] + 1,))) + data += field((3, 20), b"\x01") # width + data += field((3, 30), b"\x01") # height + data += field( + (8, 10), + bytes((info.get("data", 0),)), + ) + + return BytesIO(data) + + def test_open() -> None: expected = Image.new("L", (1, 1)) - f = BytesIO( - b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01" - b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00" - ) + f = create_iptc_image() with Image.open(f) as im: - assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] + assert im.tile == [("iptc", (0, 0, 1, 1), 25, ("raw", None))] assert_image_equal(im, expected) with Image.open(f) as im: assert im.load() is not None +def test_field_length() -> None: + f = create_iptc_image() + f.seek(28) + f.write(b"\xff") + with pytest.raises(OSError, match="illegal field length in IPTC/NAA file"): + with Image.open(f): + pass + + +@pytest.mark.parametrize("layers, mode", ((3, "RGB"), (4, "CMYK"))) +def test_layers(layers: int, mode: str) -> None: + for band in range(-1, layers): + info = {"layers": layers, "component": 1, "data": 5} + if band != -1: + info["band"] = band + f = create_iptc_image(info) + with Image.open(f) as im: + assert im.mode == mode + + data = [0] * layers + data[max(band, 0)] = 5 + assert im.getpixel((0, 0)) == tuple(data) + + +def test_unknown_compression() -> None: + f = create_iptc_image({"compression": 2}) + with pytest.raises(OSError, match="Unknown IPTC image compression"): + with Image.open(f): + pass + + +def test_getiptcinfo() -> None: + f = create_iptc_image() + with Image.open(f) as im: + assert IptcImagePlugin.getiptcinfo(im) == { + (3, 60): b"\x01\x00", + (3, 120): b"\x01", + (3, 20): b"\x01", + (3, 30): b"\x01", + } + + def test_getiptcinfo_jpg_none() -> None: # Arrange with hopper() as im: diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 85a13fe2c..c28f4dcc7 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -96,16 +96,18 @@ class IptcImageFile(ImageFile.ImageFile): # mode layers = self.info[(3, 60)][0] component = self.info[(3, 60)][1] - if (3, 65) in self.info: - id = self.info[(3, 65)][0] - 1 - else: - id = 0 if layers == 1 and not component: self._mode = "L" - elif layers == 3 and component: - self._mode = "RGB"[id] - elif layers == 4 and component: - self._mode = "CMYK"[id] + band = None + else: + if layers == 3 and component: + self._mode = "RGB" + elif layers == 4 and component: + self._mode = "CMYK" + if (3, 65) in self.info: + band = self.info[(3, 65)][0] - 1 + else: + band = 0 # size self._size = self.getint((3, 20)), self.getint((3, 30)) @@ -120,14 +122,16 @@ class IptcImageFile(ImageFile.ImageFile): # tile if tag == (8, 10): self.tile = [ - ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression) + ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band)) ] def load(self) -> Image.core.PixelAccess | None: if self.tile: - offset, compression = self.tile[0][2:] + args = self.tile[0].args + assert isinstance(args, tuple) + compression, band = args - self.fp.seek(offset) + self.fp.seek(self.tile[0].offset) # Copy image data to temporary file o = BytesIO() @@ -147,7 +151,12 @@ class IptcImageFile(ImageFile.ImageFile): size -= len(s) with Image.open(o) as _im: - _im.load() + if band is not None: + bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode) + bands[band] = _im + _im = Image.merge(self.mode, bands) + else: + _im.load() self.im = _im.im self.tile = [] return ImageFile.ImageFile.load(self) From 6fdbf5433108454f8085b5d01c803367f97535a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jul 2025 19:50:19 +1000 Subject: [PATCH 18/89] Width and height are unsigned --- src/PIL/GbrImagePlugin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index f319d7e84..d69295363 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -54,7 +54,7 @@ class GbrImageFile(ImageFile.ImageFile): width = i32(self.fp.read(4)) height = i32(self.fp.read(4)) color_depth = i32(self.fp.read(4)) - if width <= 0 or height <= 0: + if width == 0 or height == 0: msg = "not a GIMP brush" raise SyntaxError(msg) if color_depth not in (1, 4): @@ -71,7 +71,7 @@ class GbrImageFile(ImageFile.ImageFile): raise SyntaxError(msg) self.info["spacing"] = i32(self.fp.read(4)) - comment = self.fp.read(comment_length)[:-1] + self.info["comment"] = self.fp.read(comment_length)[:-1] if color_depth == 1: self._mode = "L" @@ -80,8 +80,6 @@ class GbrImageFile(ImageFile.ImageFile): self._size = width, height - self.info["comment"] = comment - # Image might not be small Image._decompression_bomb_check(self.size) From 4adff39bfd7a04c875b41bd879f513cde09c4604 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Jul 2025 19:55:58 +1000 Subject: [PATCH 19/89] Improved test coverage --- Tests/test_file_gbr.py | 51 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 1b834cd3c..b8851d82b 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,8 +1,10 @@ from __future__ import annotations +from io import BytesIO + import pytest -from PIL import GbrImagePlugin, Image +from PIL import GbrImagePlugin, Image, _binary from .helper import assert_image_equal_tofile @@ -31,8 +33,49 @@ def test_multiple_load_operations() -> None: assert_image_equal_tofile(im, "Tests/images/gbr.png") -def test_invalid_file() -> None: - invalid_file = "Tests/images/flower.jpg" +def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO: + return BytesIO( + b"".join( + _binary.o32be(i) + for i in [ + info.get("header_size", 20), + info.get("version", 1), + info.get("width", 1), + info.get("height", 1), + info.get("color_depth", 1), + ] + ) + + magic_number + ) - with pytest.raises(SyntaxError): + +def test_invalid_file() -> None: + for f in [ + create_gbr_image({"header_size": 0}), + create_gbr_image({"width": 0}), + create_gbr_image({"height": 0}), + ]: + with pytest.raises(SyntaxError, match="not a GIMP brush"): + GbrImagePlugin.GbrImageFile(f) + + invalid_file = "Tests/images/flower.jpg" + with pytest.raises(SyntaxError, match="Unsupported GIMP brush version"): GbrImagePlugin.GbrImageFile(invalid_file) + + +def test_unsupported_gimp_brush() -> None: + f = create_gbr_image({"color_depth": 2}) + with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"): + GbrImagePlugin.GbrImageFile(f) + + +def test_bad_magic_number() -> None: + f = create_gbr_image({"version": 2}, magic_number=b"badm") + with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"): + GbrImagePlugin.GbrImageFile(f) + + +def test_L() -> None: + f = create_gbr_image() + with Image.open(f) as im: + assert im.mode == "L" From d85fa7a2471b16bc547a46542f18f218b19a1c6b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 13 Jul 2025 16:13:44 +1000 Subject: [PATCH 20/89] Improved WmfImagePlugin test coverage --- Tests/test_file_wmf.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index dcf5f000f..906080d15 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -44,6 +44,18 @@ def test_load_zero_inch() -> None: pass +def test_load_unsupported_wmf() -> None: + b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x01" * 10) + with pytest.raises(SyntaxError, match="Unsupported WMF file format"): + WmfImagePlugin.WmfStubImageFile(b) + + +def test_load_unsupported() -> None: + b = BytesIO(b"\x01\x00\x00\x00") + with pytest.raises(SyntaxError, match="Unsupported file format"): + WmfImagePlugin.WmfStubImageFile(b) + + def test_render() -> None: with open("Tests/images/drawing.emf", "rb") as fp: data = fp.read() From 7516805121cc1da1d00cd6f0a22f64e0232c3541 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Jul 2025 19:29:27 +1000 Subject: [PATCH 21/89] Improved DDS test coverage --- Tests/images/unimplemented_pixel_format.dds | Bin 0 -> 132 bytes Tests/test_file_dds.py | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 Tests/images/unimplemented_pixel_format.dds diff --git a/Tests/images/unimplemented_pixel_format.dds b/Tests/images/unimplemented_pixel_format.dds new file mode 100644 index 0000000000000000000000000000000000000000..9092df8b1b5acda7e115b9ceaf6241d0f294dd1b GIT binary patch literal 132 rcmZ>930A0KU|?Vu;9_841TsJvNWhsOE|EY1sE!4nS^-SSSfCI9+g<`M literal 0 HcmV?d00001 diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 5c7a943b1..116dfa59c 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -380,21 +380,28 @@ def test_palette() -> None: assert_image_equal_tofile(im, "Tests/images/transparent.gif") +def test_unsupported_header_size() -> None: + with pytest.raises(OSError, match="Unsupported header size 0"): + with Image.open(BytesIO(b"DDS " + b"\x00" * 4)): + pass + + def test_unsupported_bitcount() -> None: - with pytest.raises(OSError): + with pytest.raises(OSError, match="Unsupported bitcount 24 for 131072"): with Image.open("Tests/images/unsupported_bitcount.dds"): pass @pytest.mark.parametrize( - "test_file", + "test_file, message", ( - "Tests/images/unimplemented_dxgi_format.dds", - "Tests/images/unimplemented_pfflags.dds", + ("Tests/images/unimplemented_dxgi_format.dds", "Unimplemented DXGI format 93"), + ("Tests/images/unimplemented_pixel_format.dds", "Unimplemented pixel format 0"), + ("Tests/images/unimplemented_pfflags.dds", "Unknown pixel format flags 8"), ), ) -def test_not_implemented(test_file: str) -> None: - with pytest.raises(NotImplementedError): +def test_not_implemented(test_file: str, message: str) -> None: + with pytest.raises(NotImplementedError, match=message): with Image.open(test_file): pass From 638eb1b9992804ca21577b5933d476b7bdbeb5d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:23:40 +1000 Subject: [PATCH 22/89] Update dependency mypy to v1.17.0 (#9092) --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 44b5badab..e81f527b8 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.16.1 +mypy==1.17.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 91bbeb5dcb47ce6d3b5b1c9969c982910ebee56b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Jul 2025 13:54:13 +1000 Subject: [PATCH 23/89] Revert iOS change until the test runs again --- Tests/test_pyroma.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index a161d3f05..c2f7fe22e 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,7 +1,5 @@ from __future__ import annotations -from importlib.metadata import metadata - import pytest from PIL import __version__ @@ -11,7 +9,7 @@ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") def test_pyroma() -> None: # Arrange - data = pyroma.projectdata.map_metadata_keys(metadata("Pillow")) + data = pyroma.projectdata.get_data(".") # Act rating = pyroma.ratings.rate(data) From a426eb55afcb9e8a069d6aba21405c0d89f69bec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Jul 2025 13:40:22 +1000 Subject: [PATCH 24/89] Remove file after test completion --- Tests/test_image_access.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index b3de5c13d..2609b1e34 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -276,10 +276,11 @@ class TestEmbeddable: except Exception: pytest.skip("Compiler could not be initialized") - with open("embed_pil.c", "w", encoding="utf-8") as fh: - home = sys.prefix.replace("\\", "\\\\") - fh.write( - f""" + try: + with open("embed_pil.c", "w", encoding="utf-8") as fh: + home = sys.prefix.replace("\\", "\\\\") + fh.write( + f""" #include "Python.h" int main(int argc, char* argv[]) @@ -301,17 +302,19 @@ int main(int argc, char* argv[]) return 0; }} """ - ) + ) - objects = compiler.compile(["embed_pil.c"]) - compiler.link_executable(objects, "embed_pil") + objects = compiler.compile(["embed_pil.c"]) + compiler.link_executable(objects, "embed_pil") - env = os.environ.copy() - env["PATH"] = sys.prefix + ";" + env["PATH"] + env = os.environ.copy() + env["PATH"] = sys.prefix + ";" + env["PATH"] - # Do not display the Windows Error Reporting dialog - getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) + # Do not display the Windows Error Reporting dialog + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) - process = subprocess.Popen(["embed_pil.exe"], env=env) - process.communicate() - assert process.returncode == 0 + process = subprocess.Popen(["embed_pil.exe"], env=env) + process.communicate() + assert process.returncode == 0 + finally: + os.remove("embed_pil.c") From a39d14648bdbd6638e7097167c4b8c2964ce3752 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Jul 2025 13:39:19 +1000 Subject: [PATCH 25/89] Updated manifest --- MANIFEST.in | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 95a6b1b92..6623f227d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,7 @@ include LICENSE include Makefile include tox.ini graft Tests +graft Tests/images graft checks graft patches graft src @@ -28,8 +29,19 @@ exclude .editorconfig exclude .readthedocs.yml exclude codecov.yml exclude renovate.json +exclude Tests/images/README.md +exclude Tests/images/crash*.tif +exclude Tests/images/string_dimension.tiff global-exclude .git* global-exclude *.pyc global-exclude *.so prune .ci prune wheels +prune winbuild/build +prune winbuild/depends +prune Tests/errors +prune Tests/images/jpeg2000 +prune Tests/images/msp +prune Tests/images/picins +prune Tests/images/sunraster +prune Tests/test-images From f4d86e4f44dfe799b0a2d6484fa53945ab89220e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Jul 2025 07:27:39 +1000 Subject: [PATCH 26/89] Use teardown_method --- Tests/test_image_access.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 2609b1e34..a847264d2 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -276,11 +276,10 @@ class TestEmbeddable: except Exception: pytest.skip("Compiler could not be initialized") - try: - with open("embed_pil.c", "w", encoding="utf-8") as fh: - home = sys.prefix.replace("\\", "\\\\") - fh.write( - f""" + with open("embed_pil.c", "w", encoding="utf-8") as fh: + home = sys.prefix.replace("\\", "\\\\") + fh.write( + f""" #include "Python.h" int main(int argc, char* argv[]) @@ -302,19 +301,20 @@ int main(int argc, char* argv[]) return 0; }} """ - ) + ) - objects = compiler.compile(["embed_pil.c"]) - compiler.link_executable(objects, "embed_pil") + objects = compiler.compile(["embed_pil.c"]) + compiler.link_executable(objects, "embed_pil") - env = os.environ.copy() - env["PATH"] = sys.prefix + ";" + env["PATH"] + env = os.environ.copy() + env["PATH"] = sys.prefix + ";" + env["PATH"] - # Do not display the Windows Error Reporting dialog - getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) + # Do not display the Windows Error Reporting dialog + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) - process = subprocess.Popen(["embed_pil.exe"], env=env) - process.communicate() - assert process.returncode == 0 - finally: - os.remove("embed_pil.c") + process = subprocess.Popen(["embed_pil.exe"], env=env) + process.communicate() + assert process.returncode == 0 + + def teardown_method(self) -> None: + os.remove("embed_pil.c") From 103a5a0b5913ab403eeaaf0b589278bc8e2b067b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Jul 2025 18:22:10 +1000 Subject: [PATCH 27/89] Fixed ZeroDivisionError --- Tests/test_imagestat.py | 10 ++++++++++ src/PIL/ImageStat.py | 13 ++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 0dfbc5a2a..0baab7ce2 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -57,3 +57,13 @@ def test_constant() -> None: assert st.rms[0] == 128 assert st.var[0] == 0 assert st.stddev[0] == 0 + + +def test_zero_count() -> None: + im = Image.new("L", (0, 0)) + + st = ImageStat.Stat(im) + + assert st.mean == [0] + assert st.rms == [0] + assert st.var == [0] diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 8bc504526..3a1044ba4 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -120,7 +120,7 @@ class Stat: @cached_property def mean(self) -> list[float]: """Average (arithmetic mean) pixel level for each band in the image.""" - return [self.sum[i] / self.count[i] for i in self.bands] + return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands] @cached_property def median(self) -> list[int]: @@ -141,13 +141,20 @@ class Stat: @cached_property def rms(self) -> list[float]: """RMS (root-mean-square) for each band in the image.""" - return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] + return [ + math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0 + for i in self.bands + ] @cached_property def var(self) -> list[float]: """Variance for each band in the image.""" return [ - (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] + ( + (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] + if self.count[i] + else 0 + ) for i in self.bands ] From 63163d065d632cb75466d554fb1d6ea27cc43577 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Jul 2025 19:59:47 +1000 Subject: [PATCH 28/89] Removed WebP feature handling --- Tests/test_features.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 520c25b46..7af3fffea 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -18,11 +18,7 @@ def test_check() -> None: for codec in features.codecs: assert features.check_codec(codec) == features.check(codec) for feature in features.features: - if "webp" in feature: - with pytest.warns(DeprecationWarning, match="webp"): - assert features.check_feature(feature) == features.check(feature) - else: - assert features.check_feature(feature) == features.check(feature) + assert features.check_feature(feature) == features.check(feature) def test_version() -> None: @@ -48,11 +44,7 @@ def test_version() -> None: for codec in features.codecs: test(codec, features.version_codec) for feature in features.features: - if "webp" in feature: - with pytest.warns(DeprecationWarning, match="webp"): - test(feature, features.version_feature) - else: - test(feature, features.version_feature) + test(feature, features.version_feature) @skip_unless_feature("libjpeg_turbo") From ec6d5efe4d02dc6d68e569abfd7523e21a89539f Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Sat, 26 Jul 2025 08:33:11 +0100 Subject: [PATCH 29/89] Deprecate ImageCmsProfile product_name and product_info (#8995) Co-authored-by: Andrew Murray --- Tests/test_imagecms.py | 14 ++++++++++++++ docs/deprecations.rst | 9 +++++++++ docs/releasenotes/12.0.0.rst | 8 +++++--- src/PIL/ImageCms.py | 14 ++++++++++---- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 55a4a87fb..8b5d88ac8 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -690,3 +690,17 @@ 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: + profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) + with pytest.warns( + DeprecationWarning, match="ImageCms.ImageCmsProfile.product_name" + ): + profile.product_name + with pytest.warns( + DeprecationWarning, match="ImageCms.ImageCmsProfile.product_info" + ): + profile.product_info + with pytest.raises(AttributeError): + profile.this_attribute_does_not_exist diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4e65dc807..3f95cf7f5 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -52,6 +52,15 @@ another mode before saving:: im = Image.new("I", (1, 1)) im.convert("I;16").save("out.png") +ImageCms.ImageCmsProfile.product_name and .product_info +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 12.0.0 + +``ImageCms.ImageCmsProfile.product_name`` and the corresponding +``.product_info`` attributes have been deprecated, and will be removed in +Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0. + Removed features ---------------- diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 68b664443..6c0cd4dba 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -110,10 +110,12 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). Deprecations ============ -TODO -^^^^ +ImageCms.ImageCmsProfile.product_name and .product_info +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +``ImageCms.ImageCmsProfile.product_name`` and the corresponding +``.product_info`` attributes have been deprecated, and will be removed in +Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0. API changes =========== diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index d3555694a..513e28acf 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -23,9 +23,10 @@ import operator import sys from enum import IntEnum, IntFlag from functools import reduce -from typing import Literal, SupportsFloat, SupportsInt, Union +from typing import Any, Literal, SupportsFloat, SupportsInt, Union from . import Image +from ._deprecate import deprecate from ._typing import SupportsRead try: @@ -233,9 +234,7 @@ class ImageCmsProfile: low-level profile object """ - self.filename = None - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info + self.filename: str | None = None if isinstance(profile, str): if sys.platform == "win32": @@ -256,6 +255,13 @@ class ImageCmsProfile: msg = "Invalid type for Profile" # type: ignore[unreachable] raise TypeError(msg) + def __getattr__(self, name: str) -> Any: + if name in ("product_name", "product_info"): + deprecate(f"ImageCms.ImageCmsProfile.{name}", 13) + return None + msg = f"'{self.__class__.__name__}' object has no attribute '{name}'" + raise AttributeError(msg) + def tobytes(self) -> bytes: """ Returns the profile in a format suitable for embedding in From 7dbcb32cbe524a8ec4c12f21c762cd7153b2b03b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 19:32:57 +1000 Subject: [PATCH 30/89] Update cygwin/cygwin-install-action action to v6 (#9108) Co-authored-by: Andrew Murray --- .github/workflows/test-cygwin.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index abfeaa77f..581cd6370 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -52,7 +52,7 @@ jobs: persist-credentials: false - name: Install Cygwin - uses: cygwin/cygwin-install-action@v5 + uses: cygwin/cygwin-install-action@v6 with: packages: > gcc-g++ @@ -89,10 +89,6 @@ jobs: with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - - name: Select Python version - run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 - - name: pip cache uses: actions/cache@v4 with: From 53b6d57b730a68ea58680483f8628c5e25301a1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 19:39:54 +1000 Subject: [PATCH 31/89] Drop support for PyPy3.10 --- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6d8acc44f..766c506e7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] + python-version: ["pypy3.11", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] architecture: ["x64"] include: # Test the oldest Python on 32-bit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4b516228..d18023dbc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,6 @@ jobs: ] python-version: [ "pypy3.11", - "pypy3.10", "3.14t", "3.14", "3.13t", From a6acc67660f4d2a157341c05d11e47e36c79802d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 21:00:26 +1000 Subject: [PATCH 32/89] Always check XMLPacket value --- Tests/test_file_libtiff.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 958e2749f..f61f79f17 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -365,8 +365,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) - if 700 in reloaded.tag_v2: - assert reloaded.tag_v2[700] == b"xmlpacket tag" + assert reloaded.tag_v2[700] == b"xmlpacket tag" def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: # issue #1765 From 283dcfc024113f6d0d8bcbb216d1735cb05a16e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 23:39:11 +1000 Subject: [PATCH 33/89] Removed unused code --- src/decode.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/decode.c b/src/decode.c index 03db1ce35..e7a6e6323 100644 --- a/src/decode.c +++ b/src/decode.c @@ -870,8 +870,6 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { if (strcmp(format, "j2k") == 0) { codec_format = OPJ_CODEC_J2K; - } else if (strcmp(format, "jpt") == 0) { - codec_format = OPJ_CODEC_JPT; } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { From 98d38a3bffe572459939cbb6ab730229b4a5a833 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:52:06 +1000 Subject: [PATCH 34/89] Updated libpng to 1.6.50 (#9058) --- .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 6b5aedb69..4519271b9 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -95,7 +95,7 @@ ARCHIVE_SDIR=pillow-depends-main # you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 -LIBPNG_VERSION=1.6.49 +LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2307fc8b2..fbff0daf2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ V = { "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", - "LIBPNG": "1.6.49", + "LIBPNG": "1.6.50", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", From e8b3c17ebc90f36c8ec326e765246814c21f1f48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Jul 2025 07:28:03 +1000 Subject: [PATCH 35/89] Updated documentation --- docs/deprecations.rst | 7 +++++-- docs/releasenotes/11.3.0.rst | 7 +++++++ docs/releasenotes/12.0.0.rst | 10 +++++++--- src/PIL/Image.py | 3 ++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 236554565..851f3e8d8 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -42,8 +42,11 @@ Image.fromarray mode parameter .. deprecated:: 11.3.0 -The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The -mode can be automatically determined from the object's shape and type instead. +Using the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` to change data types +has been deprecated. Since pixel values do not contain information about palettes or +color spaces, the parameter can still be used to place grayscale L mode data within a +P mode image, or read RGB data as YCbCr for example. If omitted, the mode will be +automatically determined from the object's shape and type. Saving I mode images as PNG ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 409d50295..5c04a0373 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -29,6 +29,13 @@ Image.fromarray mode parameter The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The mode can be automatically determined from the object's shape and type instead. +.. note:: + + Since pixel values do not contain information about palettes or color spaces, part + of this functionality was restored in Pillow 12.0.0. The parameter can be used to + place grayscale L mode data within a P mode image, or read RGB data as YCbCr for + example. + Saving I mode images as PNG ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 68b664443..19508b08a 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -134,7 +134,11 @@ TODO Other changes ============= -TODO -^^^^ +Image.fromarray mode parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +In Pillow 11.3.0, the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` was +deprecated. Part of this functionality has been restored in Pillow 12.0.0. Since pixel +values do not contain information about palettes or color spaces, the parameter can be +used to place grayscale L mode data within a P mode image, or read RGB data as YCbCr +for example. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c98630cc2..20917b1a4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3253,7 +3253,8 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: :param obj: Object with array interface :param mode: Optional mode to use when reading ``obj``. Since pixel values do not contain information about palettes or color spaces, this can be used to place - grayscale L mode data within a P mode image, or read RGB data as YCbCr. + grayscale L mode data within a P mode image, or read RGB data as YCbCr for + example. See: :ref:`concept-modes` for general information about modes. :returns: An image object. From bae97e1a2b75a1e3c01efc168d10b8d7ecdf3392 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:50:45 +1000 Subject: [PATCH 36/89] Update dependency cibuildwheel to v3.1.2 (#9118) --- .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 e1eb52eb8..823671828 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.0.1 +cibuildwheel==3.1.2 From ba5f81fb6b4bd143b2ceba6875b33870eaa366ce Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:23:39 +0300 Subject: [PATCH 37/89] Add support for Python 3.14 (#9120) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/installation/newer-versions.csv | 19 ++++++++++--------- docs/releasenotes/12.0.0.rst | 10 +++++++--- pyproject.toml | 3 ++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/installation/newer-versions.csv b/docs/installation/newer-versions.csv index 19816af58..e948dd540 100644 --- a/docs/installation/newer-versions.csv +++ b/docs/installation/newer-versions.csv @@ -1,9 +1,10 @@ -Python,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 11,Yes,Yes,Yes,Yes,Yes,,,, -Pillow 10.1 - 10.4,,Yes,Yes,Yes,Yes,Yes,,, -Pillow 10.0,,,Yes,Yes,Yes,Yes,,, -Pillow 9.3 - 9.5,,,Yes,Yes,Yes,Yes,Yes,, -Pillow 9.0 - 9.2,,,,Yes,Yes,Yes,Yes,, -Pillow 8.3.2 - 8.4,,,,Yes,Yes,Yes,Yes,Yes, -Pillow 8.0 - 8.3.1,,,,,Yes,Yes,Yes,Yes, -Pillow 7.0 - 7.2,,,,,,Yes,Yes,Yes,Yes +Python,3.14,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow 12,Yes,Yes,Yes,Yes,Yes,,,,, +Pillow 11,,Yes,Yes,Yes,Yes,Yes,,,, +Pillow 10.1 - 10.4,,,Yes,Yes,Yes,Yes,Yes,,, +Pillow 10.0,,,,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,,,,Yes,Yes,Yes,Yes,Yes,, +Pillow 9.0 - 9.2,,,,,Yes,Yes,Yes,Yes,, +Pillow 8.3.2 - 8.4,,,,,Yes,Yes,Yes,Yes,Yes, +Pillow 8.0 - 8.3.1,,,,,,Yes,Yes,Yes,Yes, +Pillow 7.0 - 7.2,,,,,,,Yes,Yes,Yes,Yes diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 6c0cd4dba..46cf64cf1 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -136,7 +136,11 @@ TODO Other changes ============= -TODO -^^^^ +Python 3.14 +^^^^^^^^^^^ -TODO +Pillow 11.3.0 had wheels built against Python 3.14 beta, available as a preview to help +others prepare for 3.14, and to ensure Pillow could be used immediately at the release +of 3.14.0 final (2025-10-07, :pep:`745`). + +Pillow 12.0.0 now officially supports Python 3.14. diff --git a/pyproject.toml b/pyproject.toml index 4e8623118..3693ddb8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Multimedia :: Graphics", @@ -206,7 +207,7 @@ lint.isort.required-imports = [ ] [tool.pyproject-fmt] -max_supported_python = "3.13" +max_supported_python = "3.14" [tool.pytest.ini_options] addopts = "-ra --color=auto" From 98d6c3bf8818849e2414ef4de8c9e02b03de3886 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 1 Aug 2025 08:22:28 +0800 Subject: [PATCH 38/89] Restore pyroma test for iOS (#9116) Co-authored-by: Andrew Murray --- Tests/test_image_access.py | 6 +++++- Tests/test_pyroma.py | 25 ++++++++++++++++++++++++- pyproject.toml | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index a847264d2..07c12594a 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -317,4 +317,8 @@ int main(int argc, char* argv[]) assert process.returncode == 0 def teardown_method(self) -> None: - os.remove("embed_pil.c") + try: + os.remove("embed_pil.c") + except FileNotFoundError: + # If the test was skipped or failed, the file won't exist + pass diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2f7fe22e..35f3fd076 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,5 +1,7 @@ from __future__ import annotations +from importlib.metadata import metadata + import pytest from PIL import __version__ @@ -7,9 +9,30 @@ from PIL import __version__ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") +def map_metadata_keys(metadata): + # Convert installed wheel metadata into canonical Core Metadata 2.4 format. + # This was a utility method in pyroma 4.3.3; it was removed in 5.0. + # This implementation is constructed from the relevant logic from + # Pyroma 5.0's `build_metadata()` implementation. This has been submitted + # upstream to Pyroma as https://github.com/regebro/pyroma/pull/116, + # so it may be possible to simplify this test in future. + data = {} + for key in set(metadata.keys()): + value = metadata.get_all(key) + key = pyroma.projectdata.normalize(key) + + if len(value) == 1: + value = value[0] + if value.strip() == "UNKNOWN": + continue + + data[key] = value + return data + + def test_pyroma() -> None: # Arrange - data = pyroma.projectdata.get_data(".") + data = map_metadata_keys(metadata("Pillow")) # Act rating = pyroma.ratings.rate(data) diff --git a/pyproject.toml b/pyproject.toml index 3693ddb8d..4980a9cb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ optional-dependencies.tests = [ "markdown2", "olefile", "packaging", - "pyroma", + "pyroma>=5", "pytest", "pytest-cov", "pytest-timeout", From 19829c3d95e9ee581d5ecd3f46e6c0af878482f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Jul 2025 18:55:45 +1000 Subject: [PATCH 39/89] Updated harfbuzz to 11.3.3 --- .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 4519271b9..f2b9a7f40 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -94,7 +94,7 @@ ARCHIVE_SDIR=pillow-depends-main # annotations have a source code patch that is required for some platforms. If # you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=11.2.1 +HARFBUZZ_VERSION=11.3.3 LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index fbff0daf2..7067fc3c4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "11.2.1", + "HARFBUZZ": "11.3.3", "JPEGTURBO": "3.1.1", "LCMS2": "2.17", "LIBAVIF": "1.3.0", From 27a7582b3541ad92df9900c2a9edcfe91c44a313 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 11:40:35 +1000 Subject: [PATCH 40/89] Moved imports into TYPE_CHECKING --- Tests/test_imagecms.py | 5 ++++- src/PIL/GimpPaletteFile.py | 5 ++++- src/PIL/Image.py | 11 ++++++++--- src/PIL/Jpeg2KImagePlugin.py | 8 ++++++-- src/PIL/JpegImagePlugin.py | 3 ++- src/PIL/PngImagePlugin.py | 6 ++++-- src/PIL/WebPImagePlugin.py | 4 +++- 7 files changed, 31 insertions(+), 11 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 8b5d88ac8..46c1baa2d 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -7,7 +7,7 @@ import shutil import sys from io import BytesIO from pathlib import Path -from typing import Any, Literal, cast +from typing import Literal, cast import pytest @@ -31,6 +31,9 @@ except ImportError: # Skipped via setup_module() pass +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Any SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" HAVE_PROFILE = os.path.exists(SRGB) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 379ffd739..016257d3d 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -17,7 +17,10 @@ from __future__ import annotations import re from io import BytesIO -from typing import IO + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO class GimpPaletteFile: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 262b5478b..b7c185e0d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -38,10 +38,9 @@ import struct import sys import tempfile import warnings -from collections.abc import Callable, Iterator, MutableMapping, Sequence +from collections.abc import MutableMapping from enum import IntEnum -from types import ModuleType -from typing import IO, Any, Literal, Protocol, cast +from typing import IO, Protocol, cast # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -64,6 +63,12 @@ try: except ImportError: ElementTree = None +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Iterator, Sequence + from types import ModuleType + from typing import Any, Literal + logger = logging.getLogger(__name__) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index e0f4ecae5..4c85dd4e2 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -18,11 +18,15 @@ from __future__ import annotations import io import os import struct -from collections.abc import Callable -from typing import IO, cast +from typing import cast from . import Image, ImageFile, ImagePalette, _binary +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import IO + class BoxReader: """ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index efe8eff3b..0d110035e 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,6 @@ import subprocess import sys import tempfile import warnings -from typing import IO, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -53,6 +52,8 @@ from .JpegPresets import presets TYPE_CHECKING = False if TYPE_CHECKING: + from typing import IO, Any + from .MpoImagePlugin import MpoImageFile # diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 1b9a89aef..d0f22f812 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -38,9 +38,8 @@ import re import struct import warnings import zlib -from collections.abc import Callable from enum import IntEnum -from typing import IO, Any, NamedTuple, NoReturn, cast +from typing import IO, NamedTuple, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -53,6 +52,9 @@ from ._util import DeferredError TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, NoReturn + from . import _imaging logger = logging.getLogger(__name__) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1716a18cc..2847fed20 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -1,7 +1,6 @@ from __future__ import annotations from io import BytesIO -from typing import IO, Any from . import Image, ImageFile @@ -12,6 +11,9 @@ try: except ImportError: SUPPORTED = False +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO, Any _VP8_MODES_BY_IDENTIFIER = { b"VP8 ": "RGB", From 0620daf860d4d7a5cff6e29079ff1f9773423dc4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 13:10:18 +1000 Subject: [PATCH 41/89] Renamed variable to not shadow import --- Tests/test_pyroma.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 35f3fd076..5871a7213 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -9,7 +9,7 @@ from PIL import __version__ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") -def map_metadata_keys(metadata): +def map_metadata_keys(md): # Convert installed wheel metadata into canonical Core Metadata 2.4 format. # This was a utility method in pyroma 4.3.3; it was removed in 5.0. # This implementation is constructed from the relevant logic from @@ -17,8 +17,8 @@ def map_metadata_keys(metadata): # upstream to Pyroma as https://github.com/regebro/pyroma/pull/116, # so it may be possible to simplify this test in future. data = {} - for key in set(metadata.keys()): - value = metadata.get_all(key) + for key in set(md.keys()): + value = md.get_all(key) key = pyroma.projectdata.normalize(key) if len(value) == 1: From ae6bb29b8207023c704490405c254808b04643dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 18:35:16 +1000 Subject: [PATCH 42/89] Removed support for NumPy 1.20 when type checking --- src/PIL/_typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 373938e71..e94045260 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -12,8 +12,8 @@ if TYPE_CHECKING: try: import numpy.typing as npt - NumpyArray = npt.NDArray[Any] # requires numpy>=1.21 - except (ImportError, AttributeError): + NumpyArray = npt.NDArray[Any] + except ImportError: pass if sys.version_info >= (3, 13): From 2ab301dcc95bee3b655aa0a2299907271b7a435a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 2 Aug 2025 15:02:20 +0300 Subject: [PATCH 43/89] Drop support for Python 3.9 (#9119) Co-authored-by: Andrew Murray Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .ci/install.sh | 67 +++++------ .github/mergify.yml | 1 - .github/workflows/test-cygwin.yml | 150 ------------------------- .github/workflows/test-windows.yml | 4 +- .github/workflows/test.yml | 11 +- README.md | 3 - Tests/helper.py | 9 +- Tests/test_features.py | 5 +- Tests/test_format_hsv.py | 5 +- Tests/test_image_transform.py | 5 +- Tests/test_imagechops.py | 6 +- Tests/test_imagecms.py | 7 +- Tests/test_imagedraw.py | 9 +- Tests/test_qt_image_qapplication.py | 42 +++---- Tests/test_qt_image_toqimage.py | 8 +- Tests/test_shell_injection.py | 8 +- checks/check_imaging_leaks.py | 3 +- docs/index.rst | 4 - docs/installation/platform-support.rst | 24 ++-- docs/reference/internal_modules.rst | 5 - docs/releasenotes/12.0.0.rst | 6 + pyproject.toml | 10 +- src/PIL/GifImagePlugin.py | 6 +- src/PIL/GimpGradientFile.py | 6 +- src/PIL/ImageDraw.py | 19 ++-- src/PIL/ImageFilter.py | 7 +- src/PIL/ImageMath.py | 8 +- src/PIL/ImageQt.py | 21 ++-- src/PIL/ImageSequence.py | 6 +- src/PIL/PcfFontFile.py | 6 +- src/PIL/PdfParser.py | 17 +-- src/PIL/TiffImagePlugin.py | 10 +- src/PIL/_imagingcms.pyi | 8 +- src/PIL/_imagingft.pyi | 3 +- src/PIL/_typing.py | 19 +--- src/PIL/_util.py | 7 +- tox.ini | 4 +- 37 files changed, 196 insertions(+), 343 deletions(-) delete mode 100644 .github/workflows/test-cygwin.yml diff --git a/.ci/install.sh b/.ci/install.sh index acb84f046..2178c6646 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -13,24 +13,21 @@ aptget_update() return 1 fi } -if [[ $(uname) != CYGWIN* ]]; then - aptget_update || aptget_update retry || aptget_update retry -fi +aptget_update || aptget_update retry || aptget_update retry set -e -if [[ $(uname) != CYGWIN* ]]; then - sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ - ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ - cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ - sway wl-clipboard libopenblas-dev nasm -fi +sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ + ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ + cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ + sway wl-clipboard libopenblas-dev nasm python3 -m pip install --upgrade pip python3 -m pip install --upgrade wheel python3 -m pip install coverage python3 -m pip install defusedxml python3 -m pip install ipython +python3 -m pip install numpy python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov @@ -40,36 +37,24 @@ python3 -m pip install pyroma # fails on beta 3.14 and PyPy python3 -m pip install --only-binary=:all: pyarrow || true -if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy - - # PyQt6 doesn't support PyPy3 - if [[ $GHA_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 - # TODO Update condition when pyqt6 supports free-threading - if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi - fi - - # Pyroma uses non-isolated build and fails with old setuptools - if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then - # To match pyproject.toml - python3 -m pip install "setuptools>=77" - fi - - # webp - pushd depends && ./install_webp.sh && popd - - # libimagequant - pushd depends && ./install_imagequant.sh && popd - - # raqm - pushd depends && ./install_raqm.sh && popd - - # libavif - pushd depends && ./install_libavif.sh && popd - - # extra test images - pushd depends && ./install_extra_test_images.sh && popd -else - cd depends && ./install_extra_test_images.sh && cd .. +# PyQt6 doesn't support PyPy3 +if [[ $GHA_PYTHON_VERSION == 3.* ]]; then + sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 + # TODO Update condition when pyqt6 supports free-threading + if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi fi + +# webp +pushd depends && ./install_webp.sh && popd + +# libimagequant +pushd depends && ./install_imagequant.sh && popd + +# raqm +pushd depends && ./install_raqm.sh && popd + +# libavif +pushd depends && ./install_libavif.sh && popd + +# extra test images +pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/mergify.yml b/.github/mergify.yml index 9bb089615..14222db10 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -8,7 +8,6 @@ pull_request_rules: - status-success=Docker Test Successful - status-success=Windows Test Successful - status-success=MinGW - - status-success=Cygwin Test Successful actions: merge: method: merge diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml deleted file mode 100644 index 581cd6370..000000000 --- a/.github/workflows/test-cygwin.yml +++ /dev/null @@ -1,150 +0,0 @@ -name: Test Cygwin - -on: - push: - branches: - - "**" - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - COVERAGE_CORE: sysmon - -jobs: - build: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-minor-version: [9] - - timeout-minutes: 40 - - name: Python 3.${{ matrix.python-minor-version }} - - steps: - - name: Fix line endings - run: | - git config --global core.autocrlf input - - - name: Checkout Pillow - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Install Cygwin - uses: cygwin/cygwin-install-action@v6 - with: - packages: > - gcc-g++ - ghostscript - git - ImageMagick - jpeg - libfreetype-devel - libimagequant-devel - libjpeg-devel - liblapack-devel - liblcms2-devel - libopenjp2-devel - libraqm-devel - libtiff-devel - libwebp-devel - libxcb-devel - libxcb-xinerama0 - make - netpbm - perl - python3${{ matrix.python-minor-version }}-cython - python3${{ matrix.python-minor-version }}-devel - python3${{ matrix.python-minor-version }}-ipython - python3${{ matrix.python-minor-version }}-numpy - python3${{ matrix.python-minor-version }}-sip - python3${{ matrix.python-minor-version }}-tkinter - wget - xorg-server-extra - zlib-devel - - - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v4 - with: - dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - - - name: pip cache - uses: actions/cache@v4 - with: - path: 'C:\cygwin\home\runneradmin\.cache\pip' - key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} - restore-keys: | - ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- - - - name: Build system information - run: | - dash.exe -c "python3 .github/workflows/system-info.py" - - - name: Install dependencies - run: | - bash.exe .ci/install.sh - - - name: Build - shell: bash.exe -eo pipefail -o igncr "{0}" - run: | - .ci/build.sh - - - name: Test - run: | - bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh - - - name: Prepare to upload errors - if: failure() - run: | - dash.exe -c "mkdir -p Tests/errors" - - - name: Upload errors - uses: actions/upload-artifact@v4 - if: failure() - with: - name: errors - path: Tests/errors - - - name: After success - run: | - bash.exe .ci/after_success.sh - rm C:\cygwin\bin\bash.EXE - - - name: Upload coverage - uses: codecov/codecov-action@v5 - with: - files: ./coverage.xml - flags: GHA_Cygwin - name: Cygwin Python 3.${{ matrix.python-minor-version }} - token: ${{ secrets.CODECOV_ORG_TOKEN }} - - success: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: Cygwin Test Successful - steps: - - name: Success - run: echo Cygwin Test Successful diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 766c506e7..c80bb6eb6 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,11 +35,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] + python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"] architecture: ["x64"] include: # Test the oldest Python on 32-bit - - { python-version: "3.9", architecture: "x86" } + - { python-version: "3.10", architecture: "x86" } timeout-minutes: 45 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d18023dbc..c075f04d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,18 +49,17 @@ jobs: "3.12", "3.11", "3.10", - "3.9", ] include: - - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - - { python-version: "3.10", PYTHONOPTIMIZE: 2 } + - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } + - { python-version: "3.11", PYTHONOPTIMIZE: 2 } # Free-threaded - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } - # M1 only available for 3.10+ - - { os: "macos-13", python-version: "3.9" } + # Intel + - { os: "macos-13", python-version: "3.10" } exclude: - - { os: "macos-latest", python-version: "3.9" } + - { os: "macos-latest", python-version: "3.10" } runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index 365d356a0..8585ef6cb 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,6 @@ As of 2019, Pillow development is GitHub Actions build status (Test MinGW) - GitHub Actions build status (Test Cygwin) GitHub Actions build status (Test Docker) diff --git a/Tests/helper.py b/Tests/helper.py index df99f5f55..e0dc8a9d4 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -10,17 +10,20 @@ import shutil import subprocess import sys import tempfile -from collections.abc import Sequence from functools import lru_cache from io import BytesIO -from pathlib import Path -from typing import Any, Callable import pytest from packaging.version import parse as parse_version from PIL import Image, ImageFile, ImageMath, features +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from pathlib import Path + from typing import Any + logger = logging.getLogger(__name__) uploader = None diff --git a/Tests/test_features.py b/Tests/test_features.py index d9212daee..93d803fc1 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,7 +2,6 @@ from __future__ import annotations import io import re -from typing import Callable import pytest @@ -10,6 +9,10 @@ from PIL import features from .helper import skip_unless_feature +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + def test_check() -> None: # Check the correctness of the convenience function diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 9cbf18566..861eccc11 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,12 +2,15 @@ from __future__ import annotations import colorsys import itertools -from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + def int_to_float(i: int) -> float: return i / 255 diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 0429eb99d..7cf52ddba 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,7 +1,6 @@ from __future__ import annotations import math -from typing import Callable import pytest @@ -9,6 +8,10 @@ from PIL import Image, ImageTransform from .helper import assert_image_equal, assert_image_similar, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + class TestImageTransform: def test_sanity(self) -> None: diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 4309214f5..61812ca7d 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Callable - from PIL import Image, ImageChops from .helper import assert_image_equal, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + BLACK = (0, 0, 0) BROWN = (127, 64, 0) CYAN = (0, 255, 255) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 46c1baa2d..5fd7caa7c 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -211,9 +211,10 @@ def test_exceptions() -> None: ImageCms.getProfileName(None) # type: ignore[arg-type] skip_missing() - # Python <= 3.9: "an integer is required (got type NoneType)" - # Python > 3.9: "'NoneType' object cannot be interpreted as an integer" - with pytest.raises(ImageCms.PyCMSError, match="integer"): + with pytest.raises( + ImageCms.PyCMSError, + match="'NoneType' object cannot be interpreted as an integer", + ): ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type] diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index e1dcbc52c..406d965b4 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,13 +1,10 @@ from __future__ import annotations import os.path -from collections.abc import Sequence -from typing import Callable import pytest from PIL import Image, ImageColor, ImageDraw, ImageFont, features -from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -17,6 +14,12 @@ from .helper import ( skip_unless_feature, ) +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from PIL._typing import Coords + BLACK = (0, 0, 0) WHITE = (255, 255, 255) GRAY = (190, 190, 190) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 82a3e0741..b31e2a4ef 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,8 +1,5 @@ from __future__ import annotations -from pathlib import Path -from typing import Union - import pytest from PIL import Image, ImageQt @@ -11,18 +8,8 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper TYPE_CHECKING = False if TYPE_CHECKING: - import PyQt6 - import PySide6 + from pathlib import Path - QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication] - QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout] - QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] - QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel] - QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter] - QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] - QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint] - QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion] - QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget] if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -32,11 +19,16 @@ if ImageQt.qt_is_installed: from PyQt6.QtGui import QImage, QPainter, QRegion from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget elif ImageQt.qt_version == "side6": - from PySide6.QtCore import QPoint - from PySide6.QtGui import QImage, QPainter, QRegion - from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget + from PySide6.QtCore import QPoint # type: ignore[assignment] + from PySide6.QtGui import QImage, QPainter, QRegion # type: ignore[assignment] + from PySide6.QtWidgets import ( # type: ignore[assignment] + QApplication, + QHBoxLayout, + QLabel, + QWidget, + ) - class Example(QWidget): # type: ignore[misc] + class Example(QWidget): def __init__(self) -> None: super().__init__() @@ -47,9 +39,9 @@ if ImageQt.qt_is_installed: pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage) # hbox - QHBoxLayout(self) # type: ignore[operator] + QHBoxLayout(self) - lbl = QLabel(self) # type: ignore[operator] + lbl = QLabel(self) # Segfault in the problem lbl.setPixmap(pixmap1.copy()) @@ -63,7 +55,7 @@ def roundtrip(expected: Image.Image) -> None: @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_sanity(tmp_path: Path) -> None: # Segfault test - app: QApplication | None = QApplication([]) # type: ignore[operator] + app: QApplication | None = QApplication([]) ex = Example() assert app # Silence warning assert ex # Silence warning @@ -84,11 +76,11 @@ def test_sanity(tmp_path: Path) -> None: imageqt = ImageQt.ImageQt(im) data = getattr(QPixmap, "fromImage")(imageqt) qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage - qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator] - painter = QPainter(qimage) # type: ignore[operator] - image_label = QLabel() # type: ignore[operator] + qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) + painter = QPainter(qimage) + image_label = QLabel() image_label.setPixmap(data) - image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator] + image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) painter.end() rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") qimage.save(rendered_tempfile) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 8cb7ffb9b..0004b5521 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,13 +1,15 @@ from __future__ import annotations -from pathlib import Path - import pytest from PIL import ImageQt from .helper import assert_image_equal, assert_image_equal_tofile, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from pathlib import Path + pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" ) @@ -21,7 +23,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) - assert isinstance(data, QImage) # type: ignore[arg-type, misc] + assert isinstance(data, QImage) assert not data.isNull() # reload directly from the qimage diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 38d46f312..465517bb6 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -2,8 +2,6 @@ from __future__ import annotations import shutil from io import BytesIO -from pathlib import Path -from typing import IO, Callable import pytest @@ -11,6 +9,12 @@ from PIL import GifImagePlugin, Image, JpegImagePlugin from .helper import djpeg_available, is_win32, netpbm_available +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + from typing import IO + TEST_JPG = "Tests/images/hopper.jpg" TEST_GIF = "Tests/images/hopper.gif" diff --git a/checks/check_imaging_leaks.py b/checks/check_imaging_leaks.py index 231789ca0..a1d59ed9c 100755 --- a/checks/check_imaging_leaks.py +++ b/checks/check_imaging_leaks.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 from __future__ import annotations -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import pytest diff --git a/docs/index.rst b/docs/index.rst index 689088d48..ee51621ac 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,10 +29,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more =2024.10.12", ] -optional-dependencies.typing = [ - "typing-extensions; python_version<'3.10'", -] optional-dependencies.xmp = [ "defusedxml", ] @@ -189,8 +185,8 @@ lint.ignore = [ "PT011", # pytest-raises-too-broad "PT012", # pytest-raises-with-multiple-statements "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 + "UP038", # pyupgrade: deprecated rule ] lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ "I002", @@ -216,7 +212,7 @@ testpaths = [ ] [tool.mypy] -python_version = "3.9" +python_version = "3.10" pretty = true disallow_any_generics = true enable_error_code = "ignore-without-code" diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b03aa7f15..58c460ef3 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, Any, Literal, NamedTuple, Union, cast +from typing import Any, NamedTuple, cast from . import ( Image, @@ -49,6 +49,8 @@ from ._util import DeferredError TYPE_CHECKING = False if TYPE_CHECKING: + from typing import IO, Literal + from . import _imaging from ._typing import Buffer @@ -535,7 +537,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image: return im.convert("L") -_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette] +_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette def _normalize_palette( diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index ec62f8e4e..5f2691882 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -21,10 +21,14 @@ See the GIMP distribution for more information.) from __future__ import annotations from math import log, pi, sin, sqrt -from typing import IO, Callable from ._binary import o8 +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import IO + EPSILON = 1e-10 """""" # Enable auto-doc for data member diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e95fa91f8..ed46899b4 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,20 +34,23 @@ from __future__ import annotations import math import struct from collections.abc import Sequence -from types import ModuleType -from typing import Any, AnyStr, Callable, Union, cast +from typing import cast from . import Image, ImageColor -from ._typing import Coords + +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from types import ModuleType + from typing import Any, AnyStr + + from . import ImageDraw2, ImageFont + from ._typing import Coords # experimental access to the outline API Outline: Callable[[], Image.core._Outline] = Image.core.outline -TYPE_CHECKING = False -if TYPE_CHECKING: - from . import ImageDraw2, ImageFont - -_Ink = Union[float, tuple[int, ...], str] +_Ink = float | tuple[int, ...] | str """ A simple 2D drawing interface for PIL images. diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index b9ed54ab2..9326eeeda 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -19,11 +19,14 @@ from __future__ import annotations import abc import functools from collections.abc import Sequence -from types import ModuleType -from typing import Any, Callable, cast +from typing import cast TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable + from types import ModuleType + from typing import Any + from . import _imaging from ._typing import NumpyArray diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index d2504b1ae..dfdc50c05 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,11 +17,15 @@ from __future__ import annotations import builtins -from types import CodeType -from typing import Any, Callable from . import Image, _imagingmath +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from types import CodeType + from typing import Any + class _Operand: """Wraps an image operand, providing standard operators""" diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index df7a57b65..af4d0742d 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,23 +19,18 @@ from __future__ import annotations import sys from io import BytesIO -from typing import Any, Callable, Union from . import Image from ._util import is_path TYPE_CHECKING = False if TYPE_CHECKING: - import PyQt6 - import PySide6 + from collections.abc import Callable + from typing import Any from . import ImageFile QBuffer: type - QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray] - QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice] - QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] - QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] qt_version: str | None qt_versions = [ @@ -49,11 +44,15 @@ for version, qt_module in qt_versions: try: qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": - from PyQt6.QtCore import QBuffer, QIODevice + from PyQt6.QtCore import QBuffer, QByteArray, QIODevice from PyQt6.QtGui import QImage, QPixmap, qRgba elif qt_module == "PySide6": - from PySide6.QtCore import QBuffer, QIODevice - from PySide6.QtGui import QImage, QPixmap, qRgba + from PySide6.QtCore import ( # type: ignore[assignment] + QBuffer, + QByteArray, + QIODevice, + ) + from PySide6.QtGui import QImage, QPixmap, qRgba # type: ignore[assignment] except (ImportError, RuntimeError): continue qt_is_installed = True @@ -183,7 +182,7 @@ def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: if qt_is_installed: - class ImageQt(QImage): # type: ignore[misc] + class ImageQt(QImage): def __init__(self, im: Image.Image | str | QByteArray) -> None: """ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index a6fc340d5..361be4897 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -16,10 +16,12 @@ ## from __future__ import annotations -from typing import Callable - from . import Image +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + class Iterator: """ diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 0d1968b14..a00e9b919 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -18,7 +18,6 @@ from __future__ import annotations import io -from typing import BinaryIO, Callable from . import FontFile, Image from ._binary import i8 @@ -27,6 +26,11 @@ from ._binary import i16le as l16 from ._binary import i32be as b32 from ._binary import i32le as l32 +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import BinaryIO + # -------------------------------------------------------------------- # declarations diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 73d8c21c0..2c9031469 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,7 +8,15 @@ import os import re import time import zlib -from typing import IO, Any, NamedTuple, Union +from typing import Any, NamedTuple + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO + + _DictBase = collections.UserDict[str | bytes, Any] +else: + _DictBase = collections.UserDict # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -251,13 +259,6 @@ class PdfArray(list[Any]): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" -TYPE_CHECKING = False -if TYPE_CHECKING: - _DictBase = collections.UserDict[Union[str, bytes], Any] -else: - _DictBase = collections.UserDict - - class PdfDict(_DictBase): def __setattr__(self, key: str, value: Any) -> None: if key == "data": diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index c1850f084..c1741284b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -47,22 +47,24 @@ import math import os import struct import warnings -from collections.abc import Iterator, MutableMapping +from collections.abc import Callable, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, Any, Callable, NoReturn, cast +from typing import IO, Any, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 -from ._typing import StrOrBytesPath from ._util import DeferredError, is_path from .TiffTags import TYPES TYPE_CHECKING = False if TYPE_CHECKING: - from ._typing import Buffer, IntegralLike + from collections.abc import Iterator + from typing import NoReturn + + from ._typing import Buffer, IntegralLike, StrOrBytesPath logger = logging.getLogger(__name__) diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi index ddcf93ab1..4fc0d60ab 100644 --- a/src/PIL/_imagingcms.pyi +++ b/src/PIL/_imagingcms.pyi @@ -1,14 +1,14 @@ import datetime import sys -from typing import Literal, SupportsFloat, TypedDict +from typing import Literal, SupportsFloat, TypeAlias, TypedDict from ._typing import CapsuleType littlecms_version: str | None -_Tuple3f = tuple[float, float, float] -_Tuple2x3f = tuple[_Tuple3f, _Tuple3f] -_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f] +_Tuple3f: TypeAlias = tuple[float, float, float] +_Tuple2x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f] +_Tuple3x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f, _Tuple3f] class _IccMeasurementCondition(TypedDict): observer: int diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 1cb1429d6..2136810ba 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,4 +1,5 @@ -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from . import ImageFont, _imaging diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index e94045260..979147e0c 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -3,7 +3,7 @@ from __future__ import annotations import os import sys from collections.abc import Sequence -from typing import Any, Protocol, TypeVar, Union +from typing import Any, Protocol, TypeVar TYPE_CHECKING = False if TYPE_CHECKING: @@ -26,19 +26,8 @@ if sys.version_info >= (3, 12): else: Buffer = Any -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: - try: - from typing_extensions import TypeGuard - except ImportError: - class TypeGuard: # type: ignore[no-redef] - def __class_getitem__(cls, item: Any) -> type[bool]: - return bool - - -Coords = Union[Sequence[float], Sequence[Sequence[float]]] +Coords = Sequence[float] | Sequence[Sequence[float]] _T_co = TypeVar("_T_co", covariant=True) @@ -48,7 +37,7 @@ class SupportsRead(Protocol[_T_co]): def read(self, length: int = ..., /) -> _T_co: ... -StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] +StrOrBytesPath = str | bytes | os.PathLike[str] | os.PathLike[bytes] -__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] +__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 8ef0d36f7..b1fa6a0f3 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,9 +1,12 @@ from __future__ import annotations import os -from typing import Any, NoReturn -from ._typing import StrOrBytesPath, TypeGuard +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Any, NoReturn, TypeGuard + + from ._typing import StrOrBytesPath def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: diff --git a/tox.ini b/tox.ini index 967d4b537..8933945b1 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 314, 313, 312, 311, 310, 39} + py{py3, 314, 313, 312, 311, 310} [testenv] deps = @@ -29,7 +29,5 @@ commands = skip_install = true deps = -r .ci/requirements-mypy.txt -extras = - typing commands = mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs} From 148e1ac914c411925df2de1972d88f7a01ccde9e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 2 Aug 2025 20:10:55 +0800 Subject: [PATCH 44/89] Add libavif support for iOS (#9117) Co-authored-by: Andrew Murray --- .github/workflows/wheels-dependencies.sh | 39 +++++++++++++++++------- checks/check_wheel.py | 3 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4519271b9..d58c65126 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -186,30 +186,43 @@ function build_libavif { python3 -m pip install meson ninja - if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then + if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IOS_SDK" ]]) || [ -n "$SANITIZER" ]; then build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03 fi local build_type=MinSizeRel + local build_shared=ON local lto=ON local libavif_cmake_flags - if [ -n "$IS_MACOS" ]; then + if [[ -n "$IS_MACOS" ]]; then lto=OFF libavif_cmake_flags=( -DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ -DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \ -DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \ ) + if [[ -n "$IOS_SDK" ]]; then + build_shared=OFF + fi else if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then build_type=Release fi libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") fi + if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then + libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic) + else + libavif_cmake_flags+=( + -DAVIF_CODEC_AOM_DECODE=OFF \ + -DAVIF_CODEC_DAV1D=LOCAL + ) + fi local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) + # CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject # of libavif) that disables support for encoding high bit depth images. (cd $out_dir \ @@ -217,20 +230,27 @@ function build_libavif { -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \ -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \ -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \ - -DBUILD_SHARED_LIBS=ON \ + -DBUILD_SHARED_LIBS=$build_shared \ -DAVIF_LIBSHARPYUV=LOCAL \ -DAVIF_LIBYUV=LOCAL \ -DAVIF_CODEC_AOM=LOCAL \ -DCONFIG_AV1_HIGHBITDEPTH=0 \ - -DAVIF_CODEC_AOM_DECODE=OFF \ - -DAVIF_CODEC_DAV1D=LOCAL \ -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \ -DCMAKE_C_VISIBILITY_PRESET=hidden \ -DCMAKE_CXX_VISIBILITY_PRESET=hidden \ -DCMAKE_BUILD_TYPE=$build_type \ "${libavif_cmake_flags[@]}" \ - . \ - && make install) + $HOST_CMAKE_FLAGS . ) + + if [[ -n "$IOS_SDK" ]]; then + # libavif's CMake configuration generates a meson cross file... but it + # doesn't work for iOS cross-compilation. Copy in Pillow-generated + # meson-cross config to replace the cmake-generated version. + cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson + fi + + (cd $out_dir && make install) + touch libavif-stamp } @@ -268,10 +288,7 @@ function build { build_tiff fi - if [[ -z "$IOS_SDK" ]]; then - # Short term workaround; don't build libavif on iOS - build_libavif - fi + build_libavif build_libpng build_lcms2 build_openjpeg diff --git a/checks/check_wheel.py b/checks/check_wheel.py index 3d806eb71..937722c4b 100644 --- a/checks/check_wheel.py +++ b/checks/check_wheel.py @@ -25,8 +25,7 @@ def test_wheel_modules() -> None: elif sys.platform == "ios": # tkinter is not available on iOS - # libavif is not available on iOS (for now) - expected_modules -= {"tkinter", "avif"} + expected_modules.remove("tkinter") assert set(features.get_supported_modules()) == expected_modules From 77247b62833afc78d561ce16ec8c34aae35f58c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 Aug 2025 12:48:47 +1000 Subject: [PATCH 45/89] Update dependency cibuildwheel to v3.1.3 (#9129) --- .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 823671828..9f9136557 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.1.2 +cibuildwheel==3.1.3 From 4677cf3b1600dae5687efe651c2814f6f0f48541 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 Aug 2025 13:58:41 +1000 Subject: [PATCH 46/89] Update dependency mypy to v1.17.1 (#9130) --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 99eac6027..bd9563800 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.17.0 +mypy==1.17.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 2973f69a756283fef2609ff473495827591b4551 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 4 Aug 2025 21:36:17 +1000 Subject: [PATCH 47/89] Updated libimagequant to 4.4.0 (#9074) --- depends/install_imagequant.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 88756f8f9..357214f1f 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -2,7 +2,7 @@ # install libimagequant archive_name=libimagequant -archive_version=4.3.4 +archive_version=4.4.0 archive=$archive_name-$archive_version diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 59c595742..fc7ef7646 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -64,7 +64,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.3.4** + * Pillow has been tested with libimagequant **2.6-4.4.0** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index fbff0daf2..4fab5f4c4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,7 +120,7 @@ V = { "JPEGTURBO": "3.1.1", "LCMS2": "2.17", "LIBAVIF": "1.3.0", - "LIBIMAGEQUANT": "4.3.4", + "LIBIMAGEQUANT": "4.4.0", "LIBPNG": "1.6.50", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.3", From cee238bcb8ca6a49e21064dd5c40440bed838503 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 06:57:50 +1000 Subject: [PATCH 48/89] [pre-commit.ci] pre-commit autoupdate (#9131) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75c7d3632..cff17cd36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.2 + rev: v0.12.7 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.7 + rev: v20.1.8 hooks: - id: clang-format types: [c] @@ -79,7 +79,7 @@ repos: additional_dependencies: [trove-classifiers>=2024.10.12] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.5.0 + rev: 1.6.0 hooks: - id: tox-ini-fmt From d3fa549ec941997dfa48e59ecf0aa3cdeb007070 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Aug 2025 18:03:47 +1000 Subject: [PATCH 49/89] Use Python 3.14 for gcc problem matching --- .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 c075f04d7..0fad22a03 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -111,7 +111,7 @@ jobs: GHA_PYTHON_VERSION: ${{ matrix.python-version }} - name: Register gcc problem matcher - if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'" + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'" run: echo "::add-matcher::.github/problem-matchers/gcc.json" - name: Build From b07dbc167c3040f076ad679c5459979cb2ca71d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Aug 2025 08:17:09 +1000 Subject: [PATCH 50/89] Fixed typo --- docs/handbook/third-party-plugins.rst | 2 +- src/PIL/WmfImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst index a189a5773..1c7dfb5e9 100644 --- a/docs/handbook/third-party-plugins.rst +++ b/docs/handbook/third-party-plugins.rst @@ -11,7 +11,7 @@ Here is a list of PyPI projects that offer additional plugins: * :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library. * :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL. * :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images. -* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implemetation. Python bindings implemented using pybind11. +* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implementation. Python bindings implemented using pybind11. * :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings. * :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format. * :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text. diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index d569cb4b8..de714d337 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -80,7 +80,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): format_description = "Windows Metafile" def _open(self) -> None: - # check placable header + # check placeable header s = self.fp.read(44) if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"): From 4f8ac76407f6dbaf0563b55700731955850170cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Aug 2025 09:00:36 +1000 Subject: [PATCH 51/89] Updated raqm to 0.10.3 --- depends/install_raqm.sh | 2 +- src/thirdparty/raqm/COPYING | 2 +- src/thirdparty/raqm/NEWS | 16 ++++++ src/thirdparty/raqm/raqm-version.h | 4 +- src/thirdparty/raqm/raqm.c | 84 ++++++++++++++++-------------- 5 files changed, 65 insertions(+), 43 deletions(-) diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index 5d862403e..b5a05100b 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=libraqm-0.10.2 +archive=libraqm-0.10.3 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING index 97e2489b7..964318a8a 100644 --- a/src/thirdparty/raqm/COPYING +++ b/src/thirdparty/raqm/COPYING @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright © 2015 Information Technology Authority (ITA) -Copyright © 2016-2023 Khaled Hosny +Copyright © 2016-2025 Khaled Hosny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS index e8bf32e0b..fb432cffb 100644 --- a/src/thirdparty/raqm/NEWS +++ b/src/thirdparty/raqm/NEWS @@ -1,3 +1,19 @@ +Overview of changes leading to 0.10.3 +Tuesday, August 5, 2025 +==================================== + +Fix raqm_set_text_utf8/utf16 reading beyond len for multibyte. + +Support building against SheenBidi 2.9. + +Fix deprecation warning with latest HarfBuzz. + +Overview of changes leading to 0.10.2 +Sunday, September 22, 2024 +==================================== + +Fix Unicode codepoint conversion from UTF-16. + Overview of changes leading to 0.10.1 Wednesday, April 12, 2023 ==================================== diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index 62d2d2064..f2dd61cf6 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -33,9 +33,9 @@ #define RAQM_VERSION_MAJOR 0 #define RAQM_VERSION_MINOR 10 -#define RAQM_VERSION_MICRO 1 +#define RAQM_VERSION_MICRO 3 -#define RAQM_VERSION_STRING "0.10.1" +#define RAQM_VERSION_STRING "0.10.3" #define RAQM_VERSION_ATLEAST(major,minor,micro) \ ((major)*10000+(minor)*100+(micro) <= \ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 2b331e1af..9ecc5cac8 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -30,7 +30,11 @@ #include #ifdef RAQM_SHEENBIDI +#ifdef RAQM_SHEENBIDI_GT_2_9 +#include +#else #include +#endif #else #ifdef HAVE_FRIBIDI_SYSTEM #include @@ -546,34 +550,32 @@ raqm_set_text (raqm_t *rq, return true; } -static void * -_raqm_get_utf8_codepoint (const void *str, +static const char * +_raqm_get_utf8_codepoint (const char *str, uint32_t *out_codepoint) { - const char *s = (const char *)str; - - if (0xf0 == (0xf8 & s[0])) + if (0xf0 == (0xf8 & str[0])) { - *out_codepoint = ((0x07 & s[0]) << 18) | ((0x3f & s[1]) << 12) | ((0x3f & s[2]) << 6) | (0x3f & s[3]); - s += 4; + *out_codepoint = ((0x07 & str[0]) << 18) | ((0x3f & str[1]) << 12) | ((0x3f & str[2]) << 6) | (0x3f & str[3]); + str += 4; } - else if (0xe0 == (0xf0 & s[0])) + else if (0xe0 == (0xf0 & str[0])) { - *out_codepoint = ((0x0f & s[0]) << 12) | ((0x3f & s[1]) << 6) | (0x3f & s[2]); - s += 3; + *out_codepoint = ((0x0f & str[0]) << 12) | ((0x3f & str[1]) << 6) | (0x3f & str[2]); + str += 3; } - else if (0xc0 == (0xe0 & s[0])) + else if (0xc0 == (0xe0 & str[0])) { - *out_codepoint = ((0x1f & s[0]) << 6) | (0x3f & s[1]); - s += 2; + *out_codepoint = ((0x1f & str[0]) << 6) | (0x3f & str[1]); + str += 2; } else { - *out_codepoint = s[0]; - s += 1; + *out_codepoint = str[0]; + str += 1; } - return (void *)s; + return str; } static size_t @@ -585,42 +587,41 @@ _raqm_u8_to_u32 (const char *text, size_t len, uint32_t *unicode) while ((*in_utf8 != '\0') && (in_len < len)) { - in_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32); + const char *out_utf8 = _raqm_get_utf8_codepoint (in_utf8, out_utf32); + in_len += out_utf8 - in_utf8; + in_utf8 = out_utf8; ++out_utf32; - ++in_len; } return (out_utf32 - unicode); } -static void * -_raqm_get_utf16_codepoint (const void *str, - uint32_t *out_codepoint) +static const uint16_t * +_raqm_get_utf16_codepoint (const uint16_t *str, + uint32_t *out_codepoint) { - const uint16_t *s = (const uint16_t *)str; - - if (s[0] > 0xD800 && s[0] < 0xDBFF) + if (str[0] >= 0xD800 && str[0] <= 0xDBFF) { - if (s[1] > 0xDC00 && s[1] < 0xDFFF) + if (str[1] >= 0xDC00 && str[1] <= 0xDFFF) { - uint32_t X = ((s[0] & ((1 << 6) -1)) << 10) | (s[1] & ((1 << 10) -1)); - uint32_t W = (s[0] >> 6) & ((1 << 5) - 1); + uint32_t X = ((str[0] & ((1 << 6) -1)) << 10) | (str[1] & ((1 << 10) -1)); + uint32_t W = (str[0] >> 6) & ((1 << 5) - 1); *out_codepoint = (W+1) << 16 | X; - s += 2; + str += 2; } else { /* A single high surrogate, this is an error. */ - *out_codepoint = s[0]; - s += 1; + *out_codepoint = str[0]; + str += 1; } } else { - *out_codepoint = s[0]; - s += 1; + *out_codepoint = str[0]; + str += 1; } - return (void *)s; + return str; } static size_t @@ -632,9 +633,10 @@ _raqm_u16_to_u32 (const uint16_t *text, size_t len, uint32_t *unicode) while ((*in_utf16 != '\0') && (in_len < len)) { - in_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); + const uint16_t *out_utf16 = _raqm_get_utf16_codepoint (in_utf16, out_utf32); + in_len += (out_utf16 - in_utf16); + in_utf16 = out_utf16; ++out_utf32; - ++in_len; } return (out_utf32 - unicode); @@ -1114,12 +1116,12 @@ _raqm_set_spacing (raqm_t *rq, { if (_raqm_allowed_grapheme_boundary (rq->text[i], rq->text[i+1])) { - /* CSS word seperators, word spacing is only applied on these.*/ + /* CSS word separators, word spacing is only applied on these.*/ if (rq->text[i] == 0x0020 || /* Space */ rq->text[i] == 0x00A0 || /* No Break Space */ rq->text[i] == 0x1361 || /* Ethiopic Word Space */ - rq->text[i] == 0x10100 || /* Aegean Word Seperator Line */ - rq->text[i] == 0x10101 || /* Aegean Word Seperator Dot */ + rq->text[i] == 0x10100 || /* Aegean Word Separator Line */ + rq->text[i] == 0x10101 || /* Aegean Word Separator Dot */ rq->text[i] == 0x1039F || /* Ugaric Word Divider */ rq->text[i] == 0x1091F) /* Phoenician Word Separator */ { @@ -2167,6 +2169,10 @@ _raqm_ft_transform (int *x, *y = vector.y; } +#if !HB_VERSION_ATLEAST (10, 4, 0) +# define hb_ft_font_get_ft_face hb_ft_font_get_face +#endif + static bool _raqm_shape (raqm_t *rq) { @@ -2199,7 +2205,7 @@ _raqm_shape (raqm_t *rq) hb_glyph_position_t *pos; unsigned int len; - FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL); + FT_Get_Transform (hb_ft_font_get_ft_face (run->font), &matrix, NULL); pos = hb_buffer_get_glyph_positions (run->buffer, &len); info = hb_buffer_get_glyph_infos (run->buffer, &len); From d975e312e288630cd25973497afdf92ffdc6ba2e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 8 Aug 2025 05:46:10 +1000 Subject: [PATCH 52/89] Updated zlib-ng to 2.2.5 --- .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 fb86b6c7d..920dd1cc6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -102,7 +102,7 @@ XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 -ZLIB_NG_VERSION=2.2.4 +ZLIB_NG_VERSION=2.2.5 LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5633519dd..86485868c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -126,7 +126,7 @@ V = { "OPENJPEG": "2.5.3", "TIFF": "4.7.0", "XZ": "5.8.1", - "ZLIBNG": "2.2.4", + "ZLIBNG": "2.2.5", } V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From b8ffea2c56808661e460ecb4bca71b8c0a81265b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 8 Aug 2025 06:05:30 +1000 Subject: [PATCH 53/89] Revert "Revert to zlib on macOS < 10.15" This reverts commit 6c7917d7a6031ae22e1d9eaccc2e536123ea25c2. --- .github/workflows/wheels-dependencies.sh | 7 +------ checks/check_wheel.py | 3 --- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 920dd1cc6..c37ef7996 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -101,7 +101,6 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 -ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.5 LIBWEBP_VERSION=1.6.0 BZIP2_VERSION=1.0.8 @@ -259,11 +258,7 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then - build_new_zlib - else - build_zlib_ng - fi + build_zlib_ng build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [[ -n "$IS_MACOS" ]]; then diff --git a/checks/check_wheel.py b/checks/check_wheel.py index 937722c4b..f716c8498 100644 --- a/checks/check_wheel.py +++ b/checks/check_wheel.py @@ -4,7 +4,6 @@ import platform import sys from PIL import features -from Tests.helper import is_pypy def test_wheel_modules() -> None: @@ -48,8 +47,6 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") - elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm": - expected_features.remove("zlib_ng") elif sys.platform == "ios": # Can't distribute raqm due to licensing, and there's no system version; # fribidi and harfbuzz won't be available if raqm isn't available. From b1cfa7769ba64c0546a25c06fc7b3289a8b041c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 07:13:41 +1000 Subject: [PATCH 54/89] Update actions/download-artifact action to v5 (#9141) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5cc4f0355..d217d9292 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -256,7 +256,7 @@ jobs: runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: dist-* path: dist @@ -278,7 +278,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: dist-* path: dist From ee8fbc0ac9510551f3dea5d24938af8be3b196de Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Aug 2025 14:58:31 +1000 Subject: [PATCH 55/89] Make in parallel when building brotli and libavif --- .github/workflows/wheels-dependencies.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index fb86b6c7d..c79cd2f17 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -165,7 +165,7 @@ function build_brotli { 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_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \ - && make install) + && make -j4 install) touch brotli-stamp } @@ -249,7 +249,7 @@ function build_libavif { cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson fi - (cd $out_dir && make install) + (cd $out_dir && make -j4 install) touch libavif-stamp } From 5a90fb81cb75c9b33f5505659a9c5aa4f9d7881a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Aug 2025 18:37:17 +1000 Subject: [PATCH 56/89] Added checks directory to mypy --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8933945b1..d58fd67b6 100644 --- a/tox.ini +++ b/tox.ini @@ -30,4 +30,4 @@ skip_install = true deps = -r .ci/requirements-mypy.txt commands = - mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs} + mypy conftest.py selftest.py setup.py checks docs src winbuild Tests {posargs} From f69c221376aebb7a2db019ae46c53814234e9a1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Aug 2025 18:56:55 +1000 Subject: [PATCH 57/89] Do not import from Tests directory --- checks/check_imaging_leaks.py | 7 ++++--- checks/check_j2k_leaks.py | 11 ++++++----- checks/check_jpeg_leaks.py | 32 +++++++++++++++++--------------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/checks/check_imaging_leaks.py b/checks/check_imaging_leaks.py index a1d59ed9c..e9f202f3d 100755 --- a/checks/check_imaging_leaks.py +++ b/checks/check_imaging_leaks.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from __future__ import annotations +import sys from collections.abc import Callable from typing import Any @@ -8,12 +9,12 @@ import pytest from PIL import Image -from .helper import is_win32 - min_iterations = 100 max_iterations = 10000 -pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") +pytestmark = pytest.mark.skipif( + sys.platform.startswith("win32"), reason="requires Unix or macOS" +) def _get_mem_usage() -> float: diff --git a/checks/check_j2k_leaks.py b/checks/check_j2k_leaks.py index bbe35b591..7103d502e 100644 --- a/checks/check_j2k_leaks.py +++ b/checks/check_j2k_leaks.py @@ -1,12 +1,11 @@ from __future__ import annotations +import sys from io import BytesIO import pytest -from PIL import Image - -from .helper import is_win32, skip_unless_feature +from PIL import Image, features # Limits for testing the leak mem_limit = 1024 * 1048576 @@ -15,8 +14,10 @@ iterations = int((mem_limit / stack_size) * 2) test_file = "Tests/images/rgb_trns_ycbc.jp2" pytestmark = [ - pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"), - skip_unless_feature("jpg_2000"), + pytest.mark.skipif( + sys.platform.startswith("win32"), reason="requires Unix or macOS" + ), + pytest.mark.skipif(not features.check("jpg_2000"), reason="jpg_2000 not available"), ] diff --git a/checks/check_jpeg_leaks.py b/checks/check_jpeg_leaks.py index 2f42ad734..2c27ce1d5 100644 --- a/checks/check_jpeg_leaks.py +++ b/checks/check_jpeg_leaks.py @@ -1,10 +1,11 @@ from __future__ import annotations +import sys from io import BytesIO import pytest -from .helper import hopper, is_win32 +from PIL import Image iterations = 5000 @@ -18,7 +19,9 @@ valgrind --tool=massif python test-installed.py -s -v checks/check_jpeg_leaks.py """ -pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") +pytestmark = pytest.mark.skipif( + sys.platform.startswith("win32"), reason="requires Unix or macOS" +) """ pre patch: @@ -112,10 +115,10 @@ standard_chrominance_qtable = ( ), ) def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: - im = hopper("RGB") - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) + with Image.open("Tests/images/hopper.ppm") as im: + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) def test_exif_leak() -> None: @@ -173,12 +176,12 @@ def test_exif_leak() -> None: 0 +----------------------------------------------------------------------->Gi 0 11.33 """ - im = hopper("RGB") exif = b"12345678" * 4096 - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", exif=exif) + with Image.open("Tests/images/hopper.ppm") as im: + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", exif=exif) def test_base_save() -> None: @@ -207,8 +210,7 @@ def test_base_save() -> None: | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: 0 +----------------------------------------------------------------------->Gi 0 7.882""" - im = hopper("RGB") - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG") + with Image.open("Tests/images/hopper.ppm") as im: + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG") From 1a5acabd32fcb505350c84e35dc78319c3f63899 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Aug 2025 19:53:05 +1000 Subject: [PATCH 58/89] Make in parallel when building libjpeg-turbo and openjpeg --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 42d761728..647393271 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155 +Subproject commit 64739327166fcad1fa41ad9b23fa910fa244c84f From 5e7f1312874dfd01a7b03af26b5f836bf0aa65ac Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:40:32 +0300 Subject: [PATCH 59/89] Add Debian 13 Trixie (#9147) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/test-docker.yml | 2 ++ docs/installation/platform-support.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 0b90732eb..47f2d3f0a 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,6 +47,8 @@ jobs: centos-stream-10-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, + debian-13-trixie-x86, + debian-13-trixie-amd64, fedora-41-amd64, fedora-42-amd64, gentoo, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 5cf0276d1..d97895c86 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,6 +31,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ +| Debian 13 Trixie | 3.13 | x86, x86-64 | ++----------------------------------+----------------------------+---------------------+ | Fedora 41 | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 42 | 3.13 | x86-64 | From a72c6318771ca8e385a5dcd5a72a721df403dc21 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Aug 2025 12:36:33 +1000 Subject: [PATCH 60/89] Updated URLs --- .github/zizmor.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 5bdc48c30..b56709781 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,5 +1,5 @@ # Configuration for the zizmor static analysis tool, run via pre-commit in CI -# https://woodruffw.github.io/zizmor/configuration/ +# https://docs.zizmor.sh/configuration/ rules: unpinned-uses: config: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cff17cd36..2be509d54 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: - id: check-readthedocs - id: check-renovate - - repo: https://github.com/woodruffw/zizmor-pre-commit + - repo: https://github.com/zizmorcore/zizmor-pre-commit rev: v1.11.0 hooks: - id: zizmor From 425a3a1af07c262f39e6f0d4fd5fa47e7f711859 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Aug 2025 11:33:02 +1000 Subject: [PATCH 61/89] Updated macOS version in CI targets --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index d97895c86..3c5e4cd51 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -41,7 +41,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | macOS 13 Ventura | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 14 Sonoma | 3.11, 3.12, 3.13, 3.14 | arm64 | +| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14 | arm64 | | | PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | From 62546924b5c890c4b7eebb163afc11fd8a71f0d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 18 Aug 2025 08:07:12 +1000 Subject: [PATCH 62/89] Remove support for FreeType <= 2.9.0 --- src/PIL/ImageFont.py | 18 +++--------------- src/_imagingft.c | 6 ------ 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index bf3f471f5..a2bf9ccf9 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -671,11 +671,7 @@ class FreeTypeFont: :returns: A list of the named styles in a variation font. :exception OSError: If the font is not a variation font. """ - try: - names = self.font.getvarnames() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + names = self.font.getvarnames() return [name.replace(b"\x00", b"") for name in names] def set_variation_by_name(self, name: str | bytes) -> None: @@ -702,11 +698,7 @@ class FreeTypeFont: :returns: A list of the axes in a variation font. :exception OSError: If the font is not a variation font. """ - try: - axes = self.font.getvaraxes() - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + axes = self.font.getvaraxes() for axis in axes: if axis["name"]: axis["name"] = axis["name"].replace(b"\x00", b"") @@ -717,11 +709,7 @@ class FreeTypeFont: :param axes: A list of values for each axis. :exception OSError: If the font is not a variation font. """ - try: - self.font.setvaraxes(axes) - except AttributeError as e: - msg = "FreeType 2.9.1 or greater is required" - raise NotImplementedError(msg) from e + self.font.setvaraxes(axes) class TransposedFont: diff --git a/src/_imagingft.c b/src/_imagingft.c index 29d8e9e71..c9938fd3e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1221,8 +1221,6 @@ glyph_error: return NULL; } -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) static PyObject * font_getvarnames(FontObject *self) { int error; @@ -1432,7 +1430,6 @@ font_setvaraxes(FontObject *self, PyObject *args) { Py_RETURN_NONE; } -#endif static void font_dealloc(FontObject *self) { @@ -1451,13 +1448,10 @@ static PyMethodDef font_methods[] = { {"render", (PyCFunction)font_render, METH_VARARGS}, {"getsize", (PyCFunction)font_getsize, METH_VARARGS}, {"getlength", (PyCFunction)font_getlength, METH_VARARGS}, -#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 9) || \ - (FREETYPE_MAJOR == 2 && FREETYPE_MINOR == 9 && FREETYPE_PATCH == 1) {"getvarnames", (PyCFunction)font_getvarnames, METH_NOARGS}, {"getvaraxes", (PyCFunction)font_getvaraxes, METH_NOARGS}, {"setvarname", (PyCFunction)font_setvarname, METH_VARARGS}, {"setvaraxes", (PyCFunction)font_setvaraxes, METH_VARARGS}, -#endif {NULL, NULL} }; From c214ad8c8d40c785c8aca6226b5033085f24cb3d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 19 Aug 2025 06:43:07 +1000 Subject: [PATCH 63/89] Use macos-14 for iOS arm64 simulator (#9161) --- .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 d217d9292..d5aacb162 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -99,7 +99,7 @@ jobs: cibw_arch: arm64_iphoneos - name: "iOS arm64 simulator" platform: ios - os: macos-latest + os: macos-14 cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios From 1435339290f8112999dfa85a07718cdac2ce2cfc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:13:56 +1000 Subject: [PATCH 64/89] Update actions/checkout action to v5 (#9156) --- .github/workflows/docs.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-valgrind-memory.yml | 2 +- .github/workflows/test-valgrind.yml | 2 +- .github/workflows/test-windows.yml | 6 +++--- .github/workflows/test.yml | 2 +- .github/workflows/wheels.yml | 8 ++++---- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 626824f38..761dc1125 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: name: Docs steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8e789a734..9827ef1cd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: name: Lint steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 47f2d3f0a..30e5c494d 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -68,7 +68,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 5a83c16c3..6c4206083 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,7 +45,7 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index e6a5f6e77..0f36fe30d 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -41,7 +41,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 8818b3b23..30caa0d4e 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -39,7 +39,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c80bb6eb6..d55a8e5f5 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -47,19 +47,19 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout cached dependencies - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false repository: python-pillow/pillow-depends path: winbuild\depends - name: Checkout extra test images - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false repository: python-pillow/test-images diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0fad22a03..b17d08892 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d5aacb162..24e78f965 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -106,7 +106,7 @@ jobs: os: macos-13 cibw_arch: x86_64_iphonesimulator steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false submodules: true @@ -153,12 +153,12 @@ jobs: - cibw_arch: ARM64 os: windows-11-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false - name: Checkout extra test images - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false repository: python-pillow/test-images @@ -234,7 +234,7 @@ jobs: if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false From c826b932c07522272eb1297a595c8c90726970db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 19 Aug 2025 15:45:42 +1000 Subject: [PATCH 65/89] Document MAXBLOCK --- docs/reference/ImageFile.rst | 1 + src/PIL/ImageFile.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 043559352..4c34ff812 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -74,5 +74,6 @@ Constants --------- .. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES +.. autodata:: PIL.ImageFile.MAXBLOCK .. autodata:: PIL.ImageFile.ERRORS :annotation: diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 27b27127e..e33b846d4 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -46,6 +46,18 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) MAXBLOCK = 65536 +""" +By default, Pillow processes image data in blocks. This helps to prevent excessive use +of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``. + +When reading an image, this is the number of bytes to read at once. + +When writing an image, this is the number of bytes to write at once. +If the image width times 4 is greater, then that will be used instead. +Plugins may also set a greater number. + +User code may set this to another number. +""" SAFEBLOCK = 1024 * 1024 From 34c651deb8390ef752425a31e1473af4d6c9db3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 08:48:38 +1000 Subject: [PATCH 66/89] Update dependency cibuildwheel to v3.1.4 (#9164) --- .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 9f9136557..d87d7956f 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.1.3 +cibuildwheel==3.1.4 From 6a3bde05a46a8326fe02fb53fcab5a6f915d7193 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Aug 2025 15:32:12 +1000 Subject: [PATCH 67/89] Do not set core to DeferredError --- src/PIL/Image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7c185e0d..683c80762 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -103,7 +103,6 @@ try: raise ImportError(msg) except ImportError as v: - core = DeferredError.new(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for From 009444f9c51d2d008fd0256769c93a7c2acb670a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Aug 2025 21:56:03 +1000 Subject: [PATCH 68/89] Improved _accept length check --- src/PIL/FliImagePlugin.py | 2 +- src/PIL/GribStubImagePlugin.py | 2 +- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 7c5bfeefa..ccb8a5953 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -30,7 +30,7 @@ from ._util import DeferredError def _accept(prefix: bytes) -> bool: return ( - len(prefix) >= 6 + len(prefix) >= 16 and i16(prefix, 4) in [0xAF11, 0xAF12] and i16(prefix, 14) in [0, 3] # flags ) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 439fc5a3e..dfa798893 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"GRIB") and prefix[7] == 1 + return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1 class GribStubImageFile(ImageFile.StubImageFile): diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 458d586c4..6b16d5385 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -39,7 +39,7 @@ logger = logging.getLogger(__name__) def _accept(prefix: bytes) -> bool: - return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] + return len(prefix) >= 2 and prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] ## diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index db34d107a..307bc97ff 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -47,7 +47,7 @@ MODES = { def _accept(prefix: bytes) -> bool: - return prefix.startswith(b"P") and prefix[1] in b"0123456fy" + return len(prefix) >= 2 and prefix.startswith(b"P") and prefix[1] in b"0123456fy" ## From 84122a20c70589ee4d68986507b9b54c91a19620 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Aug 2025 18:29:25 +1000 Subject: [PATCH 69/89] Replaced print with assert --- Tests/test_numpy.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index ef54deeeb..f6acb3aff 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -28,15 +28,13 @@ def test_numpy_to_image() -> None: a = numpy.array(data, dtype=dtype) a.shape = TEST_IMAGE_SIZE i = Image.fromarray(a) - if list(i.getdata()) != data: - print("data mismatch for", dtype) + assert list(i.getdata()) == data else: data = list(range(100)) a = numpy.array([[x] * bands for x in data], dtype=dtype) a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands i = Image.fromarray(a) - if list(i.getchannel(0).getdata()) != list(range(100)): - print("data mismatch for", dtype) + assert list(i.getchannel(0).getdata()) == list(range(100)) return i # Check supported 1-bit integer formats From 54f4a346ef89e33eec0f889569a6d280eca70656 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Aug 2025 19:06:19 +1000 Subject: [PATCH 70/89] Added has_feature_version --- Tests/helper.py | 8 ++++++++ Tests/test_file_webp_animated.py | 18 ++++++------------ Tests/test_image_quantize.py | 18 ++++++++++-------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index e0dc8a9d4..dbdd30b42 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -175,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator: return pytest.mark.skipif(not features.check(feature), reason=reason) +def has_feature_version(feature: str, required: str) -> bool: + version = features.version(feature) + assert version is not None + version_required = parse_version(required) + version_available = parse_version(version) + return version_available >= version_required + + def skip_unless_feature_version( feature: str, required: str, reason: str | None = None ) -> pytest.MarkDecorator: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 503761374..600448fb9 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -4,13 +4,13 @@ from collections.abc import Generator from pathlib import Path import pytest -from packaging.version import parse as parse_version -from PIL import GifImagePlugin, Image, WebPImagePlugin, features +from PIL import GifImagePlugin, Image, WebPImagePlugin from .helper import ( assert_image_equal, assert_image_similar, + has_feature_version, is_big_endian, skip_unless_feature, ) @@ -53,11 +53,8 @@ def test_write_animation_L(tmp_path: Path) -> None: im.load() assert_image_similar(im, orig.convert("RGBA"), 32.9) - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") + if is_big_endian() and not has_feature_version("webp", "1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") orig.seek(orig.n_frames - 1) im.seek(im.n_frames - 1) orig.load() @@ -81,11 +78,8 @@ def test_write_animation_RGB(tmp_path: Path) -> None: assert_image_equal(im, frame1.convert("RGBA")) # Compare second frame to original - if is_big_endian(): - version = features.version_module("webp") - assert version is not None - if parse_version(version) < parse_version("1.2.2"): - pytest.skip("Fails with libwebp earlier than 1.2.2") + if is_big_endian() and not has_feature_version("webp", "1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") im.seek(1) im.load() assert_image_equal(im, frame2.convert("RGBA")) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 6d313cb8c..d847c7440 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,11 +1,16 @@ from __future__ import annotations import pytest -from packaging.version import parse as parse_version -from PIL import Image, features +from PIL import Image -from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature +from .helper import ( + assert_image_similar, + has_feature_version, + hopper, + is_ppc64le, + skip_unless_feature, +) def test_sanity() -> None: @@ -23,11 +28,8 @@ def test_sanity() -> None: @skip_unless_feature("libimagequant") def test_libimagequant_quantize() -> None: image = hopper() - if is_ppc64le(): - version = features.version_feature("libimagequant") - assert version is not None - if parse_version(version) < parse_version("4"): - pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") + if is_ppc64le() and not has_feature_version("libimagequant", "4"): + pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 15) From f80ac8d6b8915a7150d6179798e284f6002b8bd9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Aug 2025 19:16:38 +1000 Subject: [PATCH 71/89] Check version independently --- Tests/test_imagefontctl.py | 81 ++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 5954de874..95af3fda8 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -7,6 +7,7 @@ from PIL import Image, ImageDraw, ImageFont from .helper import ( assert_image_equal_tofile, assert_image_similar_tofile, + has_feature_version, skip_unless_feature, ) @@ -104,11 +105,9 @@ def test_text_direction_ttb() -> None: im = Image.new(mode="RGB", size=(100, 300)) draw = ImageDraw.Draw(im) - try: - draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") target = "Tests/images/test_direction_ttb.png" assert_image_similar_tofile(im, target, 2.8) @@ -119,19 +118,17 @@ def test_text_direction_ttb_stroke() -> None: im = Image.new(mode="RGB", size=(100, 300)) draw = ImageDraw.Draw(im) - try: - draw.text( - (27, 27), - "あい", - font=ttf, - fill=500, - direction="ttb", - stroke_width=2, - stroke_fill="#0f0", - ) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + draw.text( + (27, 27), + "あい", + font=ttf, + fill=500, + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", + ) target = "Tests/images/test_direction_ttb_stroke.png" assert_image_similar_tofile(im, target, 19.4) @@ -219,14 +216,9 @@ def test_getlength( im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) - try: - assert d.textlength(text, ttf, direction) == expected - except ValueError as ex: - if ( - direction == "ttb" - and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" - ): - pytest.skip("libraqm 0.7 or greater not available") + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + assert d.textlength(text, ttf, direction) == expected @pytest.mark.parametrize("mode", ("L", "1")) @@ -242,17 +234,12 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) - try: - target = ttf.getlength("ii", mode, direction) - actual = ttf.getlength(text, mode, direction) + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + target = ttf.getlength("ii", mode, direction) + actual = ttf.getlength(text, mode, direction) - assert actual == target - except ValueError as ex: - if ( - direction == "ttb" - and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction" - ): - pytest.skip("libraqm 0.7 or greater not available") + assert actual == target @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) @@ -265,11 +252,9 @@ def test_anchor_ttb(anchor: str) -> None: d = ImageDraw.Draw(im) d.line(((0, 200), (200, 200)), "gray") d.line(((100, 0), (100, 400)), "gray") - try: - d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f) assert_image_similar_tofile(im, path, 1) # fails at 5 @@ -310,10 +295,12 @@ combine_tests = ( # this tests various combining characters for anchor alignment and clipping @pytest.mark.parametrize( - "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] + "name, text, anchor, direction, epsilon", + combine_tests, + ids=[r[0] for r in combine_tests], ) def test_combine( - name: str, text: str, dir: str | None, anchor: str | None, epsilon: float + name: str, text: str, direction: str | None, anchor: str | None, epsilon: float ) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -322,11 +309,9 @@ def test_combine( d = ImageDraw.Draw(im) d.line(((0, 200), (400, 200)), "gray") d.line(((200, 0), (200, 400)), "gray") - try: - d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - pytest.skip("libraqm 0.7 or greater not available") + if direction == "ttb" and not has_feature_version("raqm", "0.7"): + pytest.skip("libraqm 0.7 or greater not available") + d.text((200, 200), text, fill="black", anchor=anchor, direction=direction, font=f) assert_image_similar_tofile(im, path, epsilon) From a59ce257e9ccc966d0a4a2b57a1ef2f05134f9b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Aug 2025 19:37:26 +1000 Subject: [PATCH 72/89] Install zstd for libtiff on Linux --- .github/workflows/wheels-dependencies.sh | 10 ++++++++ wheels/dependency_licenses/ZSTD.txt | 30 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 wheels/dependency_licenses/ZSTD.txt diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index c79cd2f17..72934a9b9 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -99,6 +99,7 @@ LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 +ZSTD_VERSION=1.5.7 TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 @@ -254,6 +255,14 @@ function build_libavif { touch libavif-stamp } +function build_zstd { + if [ -e zstd-stamp ]; then return; fi + local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz) + (cd $out_dir \ + && make -j4 install) + touch zstd-stamp +} + function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then @@ -285,6 +294,7 @@ function build { --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ --disable-webp --disable-libdeflate --disable-zstd else + build_zstd build_tiff fi diff --git a/wheels/dependency_licenses/ZSTD.txt b/wheels/dependency_licenses/ZSTD.txt new file mode 100644 index 000000000..75800288c --- /dev/null +++ b/wheels/dependency_licenses/ZSTD.txt @@ -0,0 +1,30 @@ +BSD License + +For Zstandard software + +Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook, nor Meta, nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From c7a268e5a5d026d17374309a0fde23cbcc0f8bf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 1 Sep 2025 08:23:30 +1000 Subject: [PATCH 73/89] ImageMorph operations must have length 1 (#9102) --- Tests/test_imagemorph.py | 14 ++++++++------ docs/releasenotes/12.0.0.rst | 7 +++++++ src/PIL/ImageMorph.py | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 515e29cea..ca192a809 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -7,7 +7,7 @@ import pytest from PIL import Image, ImageMorph, _imagingmorph -from .helper import assert_image_equal_tofile, hopper +from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower_valgrind def string_to_img(image_string: str) -> Image.Image: @@ -266,16 +266,18 @@ def test_unknown_pattern() -> None: ImageMorph.LutBuilder(op_name="unknown") -def test_pattern_syntax_error() -> None: +@pytest.mark.parametrize( + "pattern", ("a pattern with a syntax error", "4:(" + "X" * 30000) +) +@timeout_unless_slower_valgrind(1) +def test_pattern_syntax_error(pattern: str) -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") - new_patterns = ["a pattern with a syntax error"] + new_patterns = [pattern] lb.add_patterns(new_patterns) # Act / Assert - with pytest.raises( - Exception, match='Syntax error in pattern "a pattern with a syntax error"' - ): + with pytest.raises(Exception, match='Syntax error in pattern "'): lb.build_lut() diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index e21c243ea..41edea318 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -150,3 +150,10 @@ others prepare for 3.14, and to ensure Pillow could be used immediately at the r of 3.14.0 final (2025-10-07, :pep:`745`). Pillow 12.0.0 now officially supports Python 3.14. + +ImageMorph operations must have length 1 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Valid ImageMorph operations are 4, N, 1 and M. By limiting the length to 1 character +within Pillow, long execution times can be avoided if a user provided long pattern +strings. Reported by Jang Choi. diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index f0a066b5b..bd70aff7b 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -150,7 +150,7 @@ class LutBuilder: # Parse and create symmetries of the patterns strings for p in self.patterns: - m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) + m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) if not m: msg = 'Syntax error in pattern "' + p + '"' raise Exception(msg) From 31eee6e5f706cd0a41ac26c45694adef1eca72a3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:57:54 +1000 Subject: [PATCH 74/89] [pre-commit.ci] pre-commit autoupdate (#9180) --- .pre-commit-config.yaml | 10 +++++----- src/libImaging/Palette.c | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2be509d54..23bda1ec7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.12.11 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.8 + rev: v21.1.0 hooks: - id: clang-format types: [c] @@ -36,7 +36,7 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable @@ -51,14 +51,14 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.2 + rev: 0.33.3 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.11.0 + rev: v1.12.1 hooks: - id: zizmor diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 78916bca5..da1d80504 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -148,7 +148,7 @@ ImagingPaletteDelete(ImagingPalette palette) { #define BOX 8 -#define BOXVOLUME BOX *BOX *BOX +#define BOXVOLUME BOX * BOX * BOX void ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { From 0e22b0ca6c9577fcd5be0013ce6d10e0ee28999a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 18:33:52 +1000 Subject: [PATCH 75/89] Removed unused code --- Tests/test_font_crash.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index b82340ef7..fb5026ee0 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -2,7 +2,7 @@ from __future__ import annotations import pytest -from PIL import Image, ImageDraw, ImageFont +from PIL import ImageFont from .helper import skip_unless_feature @@ -12,10 +12,6 @@ class TestFontCrash: # from fuzzers.fuzz_font font.getbbox("ABC") font.getmask("test text") - with Image.new(mode="RGBA", size=(200, 200)) as im: - draw = ImageDraw.Draw(im) - draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) - draw.text((10, 10), "Test Text", font=font, fill="#000") @skip_unless_feature("freetype2") def test_segfault(self) -> None: From caede14465b664c542eff9365afb128d0b19a729 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 21:46:54 +1000 Subject: [PATCH 76/89] Revert "Removed unused code" This reverts commit 0e22b0ca6c9577fcd5be0013ce6d10e0ee28999a. --- Tests/test_font_crash.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index fb5026ee0..b82340ef7 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -2,7 +2,7 @@ from __future__ import annotations import pytest -from PIL import ImageFont +from PIL import Image, ImageDraw, ImageFont from .helper import skip_unless_feature @@ -12,6 +12,10 @@ class TestFontCrash: # from fuzzers.fuzz_font font.getbbox("ABC") font.getmask("test text") + with Image.new(mode="RGBA", size=(200, 200)) as im: + draw = ImageDraw.Draw(im) + draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "Test Text", font=font, fill="#000") @skip_unless_feature("freetype2") def test_segfault(self) -> None: From abf088fae57ff5fb8476652531c84755fd7d2bdd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 21:52:27 +1000 Subject: [PATCH 77/89] Updated comment --- Tests/test_font_crash.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index b82340ef7..54bd2d183 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -9,7 +9,8 @@ from .helper import skip_unless_feature class TestFontCrash: def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: - # from fuzzers.fuzz_font + # Copy of the code from fuzz_font() in Tests/oss-fuzz/fuzzers.py + # that triggered a problem when fuzzing font.getbbox("ABC") font.getmask("test text") with Image.new(mode="RGBA", size=(200, 200)) as im: From 877707379bda7923de612a4ed4116fd1ec3b6017 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Sep 2025 22:38:37 +1000 Subject: [PATCH 78/89] Deprecate Image._show --- Tests/test_image.py | 8 ++++++++ docs/deprecations.rst | 8 ++++++++ docs/releasenotes/12.0.0.rst | 6 ++++++ src/PIL/Image.py | 5 ++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index be7ca6a6f..eb3882ddc 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -19,6 +19,7 @@ from PIL import ( ImageDraw, ImageFile, ImagePalette, + ImageShow, UnidentifiedImageError, features, ) @@ -1047,6 +1048,13 @@ class TestImage: with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"): assert im.get_child_images() == [] + def test_show(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageShow, "_viewers", []) + + im = Image.new("RGB", (1, 1)) + with pytest.warns(DeprecationWarning, match="Image._show"): + Image._show(im) + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) def test_zero_tobytes(self, size: tuple[int, int]) -> None: im = Image.new("RGB", size) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 3f95cf7f5..e31d3c31c 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -61,6 +61,14 @@ ImageCms.ImageCmsProfile.product_name and .product_info ``.product_info`` attributes have been deprecated, and will be removed in Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0. +Image._show +~~~~~~~~~~~ + +.. deprecated:: 12.0.0 + +``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15). +Use :py:meth:`~PIL.ImageShow.show` instead. + Removed features ---------------- diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 41edea318..12bf760e2 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -116,6 +116,12 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). Deprecations ============ +Image._show +^^^^^^^^^^^ + +``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15). +Use :py:meth:`~PIL.ImageShow.show` instead. + ImageCms.ImageCmsProfile.product_name and .product_info ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 354118a87..5a457803b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2632,7 +2632,9 @@ class Image: :param title: Optional title to use for the image window, where possible. """ - _show(self, title=title) + from . import ImageShow + + ImageShow.show(self, title) def split(self) -> tuple[Image, ...]: """ @@ -3797,6 +3799,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: def _show(image: Image, **options: Any) -> None: from . import ImageShow + deprecate("Image._show", 13, "ImageShow.show") ImageShow.show(image, **options) From f0bbab94a6da39b0366d0f55c9f033c6ab335d28 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 07:23:15 +1000 Subject: [PATCH 79/89] Updated libjpeg-turbo to 3.1.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 c79cd2f17..b4309e8d9 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -96,7 +96,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.3.3 LIBPNG_VERSION=1.6.50 -JPEGTURBO_VERSION=3.1.1 +JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 TIFF_VERSION=4.7.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5633519dd..7539cff82 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ V = { "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "11.3.3", - "JPEGTURBO": "3.1.1", + "JPEGTURBO": "3.1.2", "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.4.0", From e0da1a62ec120cba1ae32a38880dd7c749051bda Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 08:10:31 +1000 Subject: [PATCH 80/89] Use walrus operator --- src/PIL/WalImageFile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 87e32878b..5494f62e8 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -49,8 +49,7 @@ class WalImageFile(ImageFile.ImageFile): # strings are null-terminated self.info["name"] = header[:32].split(b"\0", 1)[0] - next_name = header[56 : 56 + 32].split(b"\0", 1)[0] - if next_name: + if next_name := header[56 : 56 + 32].split(b"\0", 1)[0]: self.info["next_name"] = next_name def load(self) -> Image.core.PixelAccess | None: From cfca02a75970cc2816ce341eae5099e1465c6ac9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 08:27:52 +1000 Subject: [PATCH 81/89] Improved WAL test coverage --- Tests/test_file_wal.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index b15d79d61..549d47054 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,5 +1,7 @@ from __future__ import annotations +from io import BytesIO + from PIL import WalImageFile from .helper import assert_image_equal_tofile @@ -13,12 +15,22 @@ def test_open() -> None: assert im.format_description == "Quake2 Texture" assert im.mode == "P" assert im.size == (128, 128) + assert "next_name" not in im.info assert isinstance(im, WalImageFile.WalImageFile) assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") +def test_next_name() -> None: + with open(TEST_FILE, "rb") as fp: + data = bytearray(fp.read()) + data[56:60] = b"Test" + f = BytesIO(data) + with WalImageFile.open(f) as im: + assert im.info["next_name"] == b"Test" + + def test_load() -> None: with WalImageFile.open(TEST_FILE) as im: px = im.load() From 73490e10ad7dd7821aed94ee088cef82659a9fa1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 21:00:13 +1000 Subject: [PATCH 82/89] Mention Pillow 11.3.0 behaviour --- docs/deprecations.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8f7800ba5..a3c2c55db 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -35,11 +35,12 @@ Image.fromarray mode parameter .. deprecated:: 11.3.0 -Using the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` to change data types -has been deprecated. Since pixel values do not contain information about palettes or -color spaces, the parameter can still be used to place grayscale L mode data within a -P mode image, or read RGB data as YCbCr for example. If omitted, the mode will be -automatically determined from the object's shape and type. +Using the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` was deprecated in +Pillow 11.3.0. In Pillow 12.0.0, this was partially reverted, and it is now only +deprecated when changing data types. Since pixel values do not contain information +about palettes or color spaces, the parameter can still be used to place grayscale L +mode data within a P mode image, or read RGB data as YCbCr for example. If omitted, the +mode will be automatically determined from the object's shape and type. Saving I mode images as PNG ^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 5de27c6258f9c4c7a3686d6e2ae9ce07c4ec1138 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Sep 2025 21:09:00 +1000 Subject: [PATCH 83/89] Split versionadded info --- docs/reference/ImageGrab.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index f6a2ec5bc..5c3a73fad 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -20,7 +20,9 @@ or the clipboard to a PIL image memory. used as a fallback if they are installed. To disable this behaviour, pass ``xdisplay=""`` instead. - .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) + .. versionadded:: 1.1.3 Windows support + .. versionadded:: 3.0.0 macOS support + .. versionadded:: 7.1.0 Linux support :param bbox: What region to copy. Default is the entire screen. On macOS, this is not increased to 2x for Retina screens, so the full @@ -53,7 +55,9 @@ or the clipboard to a PIL image memory. On Linux, ``wl-paste`` or ``xclip`` is required. - .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) + .. versionadded:: 1.1.4 Windows support + .. versionadded:: 3.3.0 macOS support + .. versionadded:: 9.4.0 Linux support :return: On Windows, an image, a list of filenames, or None if the clipboard does not contain image data or filenames. From 54d329f98f214bbaf6ee23df0aec91da7f03f035 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:26:47 +1000 Subject: [PATCH 84/89] Updated harfbuzz to 11.4.5 (#9150) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .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 cbd8534aa..1fa634096 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -94,7 +94,7 @@ ARCHIVE_SDIR=pillow-depends-main # annotations have a source code patch that is required for some platforms. If # you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 -HARFBUZZ_VERSION=11.3.3 +HARFBUZZ_VERSION=11.4.5 LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.2 OPENJPEG_VERSION=2.5.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4ba683801..ba69878bc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ V = { "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "11.3.3", + "HARFBUZZ": "11.4.5", "JPEGTURBO": "3.1.2", "LCMS2": "2.17", "LIBAVIF": "1.3.0", From d4ed512bec3258d38a9debd62cdd08fe86f4c27c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 Sep 2025 23:14:52 +1000 Subject: [PATCH 85/89] Use monkeypatch --- Tests/test_imageshow.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 7a2f58767..8d6731acc 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -59,15 +59,12 @@ def test_show(mode: str) -> None: assert ImageShow.show(im) -def test_show_without_viewers() -> None: - viewers = ImageShow._viewers - ImageShow._viewers = [] +def test_show_without_viewers(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ImageShow, "_viewers", []) with hopper() as im: assert not ImageShow.show(im) - ImageShow._viewers = viewers - @pytest.mark.parametrize( "viewer", From a58fc562f08ece7763824fea1e5ee02ed3000024 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:55:35 +1000 Subject: [PATCH 86/89] Update github-actions (#9194) --- .github/workflows/docs.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 2 +- .github/workflows/wheels.yml | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 761dc1125..cf917407c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" cache: pip diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9827ef1cd..2addbaf67 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,7 +33,7 @@ jobs: lint-pre-commit- - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" cache: pip diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 61ccf58e2..1b0c3c654 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -22,7 +22,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v9 + uses: actions/stale@v10 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index d55a8e5f5..e12a5b1f7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -67,7 +67,7 @@ jobs: # sets env: pythonLocation - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b17d08892..8504e5c1e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 24e78f965..81a688135 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -111,7 +111,7 @@ jobs: persist-credentials: false submodules: true - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -164,7 +164,7 @@ jobs: repository: python-pillow/test-images path: Tests\test-images - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.x" @@ -239,7 +239,7 @@ jobs: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" From 2d8244c45adeab9fecdf7e1fa3adc9af175a67d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Sep 2025 23:39:04 +1000 Subject: [PATCH 87/89] Added GitHub profile link --- docs/releasenotes/12.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index b166f51b3..0a03b982f 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -171,4 +171,4 @@ ImageMorph operations must have length 1 Valid ImageMorph operations are 4, N, 1 and M. By limiting the length to 1 character within Pillow, long execution times can be avoided if a user provided long pattern -strings. Reported by Jang Choi. +strings. Reported by Jang Choi (https://github.com/uko3211). From 4b8bcb6f379e8073519b3afff745156542f78258 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:04:01 +1000 Subject: [PATCH 88/89] Use link Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/releasenotes/12.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 0a03b982f..de9d6dffd 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -171,4 +171,4 @@ ImageMorph operations must have length 1 Valid ImageMorph operations are 4, N, 1 and M. By limiting the length to 1 character within Pillow, long execution times can be avoided if a user provided long pattern -strings. Reported by Jang Choi (https://github.com/uko3211). +strings. Reported by `Jang Choi `__. From d70cba37627586b243a9b3aeac7899f5389d3ba8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:55:50 +1000 Subject: [PATCH 89/89] Update dependency mypy to v1.18.1 (#9207) --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index bd9563800..68d69c183 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.17.1 +mypy==1.18.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython