From 99737228c5a65d5291ecdf4d9718a34a815e8b32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Jul 2025 06:53:22 +1000 Subject: [PATCH 01/30] 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 74e36e0ee5da824132595c2c13dc1fc8416c743f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 16:48:46 +1000 Subject: [PATCH 02/30] 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 bc2519abf10e6a2a095be82d7a268870afaaba71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Jul 2025 22:45:22 +1000 Subject: [PATCH 03/30] 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 04/30] 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 05/30] 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 a6acc67660f4d2a157341c05d11e47e36c79802d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 21:00:26 +1000 Subject: [PATCH 06/30] 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 07/30] 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 e8b3c17ebc90f36c8ec326e765246814c21f1f48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 29 Jul 2025 07:28:03 +1000 Subject: [PATCH 08/30] 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 0620daf860d4d7a5cff6e29079ff1f9773423dc4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 13:10:18 +1000 Subject: [PATCH 09/30] 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 4f8ac76407f6dbaf0563b55700731955850170cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 Aug 2025 09:00:36 +1000 Subject: [PATCH 10/30] 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 11/30] 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 12/30] 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 a59ce257e9ccc966d0a4a2b57a1ef2f05134f9b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Aug 2025 19:37:26 +1000 Subject: [PATCH 13/30] 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 14/30] 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 15/30] [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 16/30] 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 17/30] 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 18/30] 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 19/30] 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 20/30] 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 21/30] 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 22/30] 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 23/30] 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 24/30] 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 25/30] 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 26/30] 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 27/30] 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 28/30] 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 29/30] 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 30/30] 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