From 4f5802b6b17a3c463f734e3c5cc3484dca9b1d44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Mar 2026 23:45:22 +1100 Subject: [PATCH 01/34] Do not use palette from grayscale or bilevel colorspace --- Tests/test_file_jpeg2k.py | 7 +++++++ src/PIL/Jpeg2KImagePlugin.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 575d911de..4f6376ed7 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -440,6 +440,13 @@ def test_pclr() -> None: assert len(im.palette.colors) == 256 assert im.palette.colors[(255, 255, 255)] == 0 + for enumcs in (0, 15, 17): + with open(f"{EXTRA_DIR}/issue104_jpxstream.jp2", "rb") as fp: + data = bytearray(fp.read()) + data[114:115] = bytes([enumcs]) + with Image.open(BytesIO(data)) as im: + assert im.mode == "L" + with Image.open( f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" ) as im: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index d6ec38d43..b08c466e2 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -176,6 +176,7 @@ def _parse_jp2_header( nc = None dpi = None # 2-tuple of DPI info, or None palette = None + colr = None while header.has_next_box(): tbox = header.next_box_type() @@ -196,11 +197,17 @@ def _parse_jp2_header( mode = "RGB" elif nc == 4: mode = "RGBA" - elif tbox == b"colr" and nc == 4: + elif tbox == b"colr": meth, _, _, enumcs = header.read_fields(">BBBI") - if meth == 1 and enumcs == 12: - mode = "CMYK" - elif tbox == b"pclr" and mode in ("L", "LA"): + if meth == 1: + if enumcs in (0, 15): + colr = "1" + elif enumcs == 12: + if nc == 4: + mode = "CMYK" + elif enumcs == 17: + colr = "L" + elif tbox == b"pclr" and mode in ("L", "LA") and colr not in ("1", "L"): ne, npc = header.read_fields(">HB") assert isinstance(ne, int) assert isinstance(npc, int) From 0d7f5077a75341b6af35636541e97299a1c5af69 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Mar 2026 00:58:27 +1100 Subject: [PATCH 02/34] If v2 extension area specifies no alpha, fill alpha channel --- Tests/test_file_tga.py | 22 +++++++++++++++++++++- src/PIL/TgaImagePlugin.py | 16 ++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 277515fd4..7ec562342 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,11 +1,12 @@ from __future__ import annotations import os +from io import BytesIO from pathlib import Path import pytest -from PIL import Image, UnidentifiedImageError +from PIL import Image, UnidentifiedImageError, _binary from .helper import assert_image_equal, assert_image_equal_tofile, hopper @@ -92,6 +93,25 @@ def test_rgba_16() -> None: assert im.getpixel((1, 0)) == (0, 255, 82, 0) +def test_v2_no_alpha() -> None: + test_file = "Tests/images/tga/common/200x32_rgba_tl_rle.tga" + with open(test_file, "rb") as fp: + data = fp.read() + data += ( + b"\x00" * 495 + + _binary.o32le(len(data)) + + _binary.o32le(0) + + b"TRUEVISION-XFILE.\x00" + ) + with Image.open(BytesIO(data)) as im: + with Image.open(test_file) as im2: + r, g, b = im2.split()[:3] + a = Image.new("L", im2.size, 255) + expected = Image.merge("RGBA", (r, g, b, a)) + + assert_image_equal(im, expected) + + def test_id_field() -> None: # tga file with id field test_file = "Tests/images/tga_id_field.tga" diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 90d5b5cf4..b2989a4b7 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -17,11 +17,13 @@ # from __future__ import annotations +import os import warnings from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 +from ._binary import i32le as i32 from ._binary import o8 from ._binary import o16le as o16 @@ -157,6 +159,20 @@ class TgaImageFile(ImageFile.ImageFile): pass # cannot decode def load_end(self) -> None: + if self.mode == "RGBA": + assert self.fp is not None + self.fp.seek(-26, os.SEEK_END) + footer = self.fp.read(26) + if footer.endswith(b"TRUEVISION-XFILE.\x00"): + # version 2 + extension_offset = i32(footer) + if extension_offset: + self.fp.seek(extension_offset + 494) + attributes_type = self.fp.read(1) + if attributes_type == b"\x00": + # No alpha + self.im.fillband(3, 255) + if self._flip_horizontally: self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) From 4e85badfc1cf83418744386f5da26db5e309a207 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Mar 2026 21:23:24 +1100 Subject: [PATCH 03/34] Updated freetype to 2.14.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 dbb7bc977..76498d5f3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -90,7 +90,7 @@ fi ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds. -FREETYPE_VERSION=2.14.2 +FREETYPE_VERSION=2.14.3 HARFBUZZ_VERSION=13.2.1 LIBPNG_VERSION=1.6.55 JPEGTURBO_VERSION=3.1.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4a8138206..4183d92b5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -114,7 +114,7 @@ ARCHITECTURES = { V = { "BROTLI": "1.2.0", - "FREETYPE": "2.14.2", + "FREETYPE": "2.14.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "13.2.1", "JPEGTURBO": "3.1.3", From 3a83d6abc3d2f51efc5b16e4f7d0351a1f9e877d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:54:16 +0200 Subject: [PATCH 04/34] Enable colour in CI logs (#9486) --- .github/workflows/cifuzz.yml | 3 +++ .github/workflows/release-drafter.yml | 3 +++ .github/workflows/stale.yml | 3 +++ .github/workflows/test-docker.yml | 3 +++ .github/workflows/test-mingw.yml | 1 + .github/workflows/test-valgrind-memory.yml | 3 +++ .github/workflows/test-valgrind.yml | 3 +++ .github/workflows/test-windows.yml | 1 + 8 files changed, 20 insertions(+) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 7e771f1b7..3f78c98b6 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -24,6 +24,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: Fuzzing: runs-on: ubuntu-latest diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a8ddef22c..12633284f 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -14,6 +14,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: update_release_draft: permissions: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1b0c3c654..9d1902838 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,6 +12,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: stale: if: github.repository_owner == 'python-pillow' diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 8f24bef3d..08226738e 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index e247414c8..808373a65 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -28,6 +28,7 @@ concurrency: env: COVERAGE_CORE: sysmon + FORCE_COLOR: 1 jobs: build: diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index bd244aa5a..87eace643 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 81cfb8456..f14dab616 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -24,6 +24,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 3bc70e337..45392a689 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -28,6 +28,7 @@ concurrency: env: COVERAGE_CORE: sysmon + FORCE_COLOR: 1 jobs: build: From 47386d191ca5b3a82ff7c2fabb3a440db921c48c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 22:33:37 +1100 Subject: [PATCH 05/34] Set image pixels individually on 32-bit Windows --- src/libImaging/Paste.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 25941ab3d..f01bce933 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -352,16 +352,16 @@ ImagingPaste( static inline void fill( - Imaging imOut, const void *ink_, int dx, int dy, int xsize, int ysize, int pixelsize + Imaging imOut, const UINT8 *ink, int dx, int dy, int xsize, int ysize, int pixelsize ) { /* fill opaque region */ - int x, y; + int x, y, i; UINT8 ink8 = 0; INT32 ink32 = 0L; - memcpy(&ink32, ink_, pixelsize); - memcpy(&ink8, ink_, sizeof(ink8)); + memcpy(&ink32, ink, pixelsize); + memcpy(&ink8, ink, sizeof(ink8)); if (imOut->image8 || ink32 == 0L) { dx *= pixelsize; @@ -371,12 +371,24 @@ fill( } } else { +#if defined _WIN32 && !defined _WIN64 + dx *= pixelsize; + for (y = 0; y < ysize; y++) { + UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx; + for (x = 0; x < xsize; x++) { + for (i = 0; i < pixelsize; i++) { + *out++ = ink[i]; + } + } + } +#else for (y = 0; y < ysize; y++) { INT32 *out = imOut->image32[y + dy] + dx; for (x = 0; x < xsize; x++) { out[x] = ink32; } } +#endif } } From 9a89944e735283ce4bbdf2dde1ea7c44798d9683 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:00:18 +0200 Subject: [PATCH 06/34] Fix `_getxy` refcount leaks (#9487) --- src/_imaging.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index d2a195887..b8b8df5c2 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1216,7 +1216,9 @@ _getxy(PyObject *xy, int *x, int *y) { PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); if (int_value != NULL && PyLong_Check(int_value)) { *x = PyLong_AS_LONG(int_value); + Py_DECREF(int_value); } else { + Py_XDECREF(int_value); goto badval; } } @@ -1230,7 +1232,9 @@ _getxy(PyObject *xy, int *x, int *y) { PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); if (int_value != NULL && PyLong_Check(int_value)) { *y = PyLong_AS_LONG(int_value); + Py_DECREF(int_value); } else { + Py_XDECREF(int_value); goto badval; } } From 93729a006271c5757dc706a67f3077817bdf32b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 23:04:35 +1100 Subject: [PATCH 07/34] Removed unused code --- src/encode.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/encode.c b/src/encode.c index f2bb464fa..b9feb28d2 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1343,8 +1343,6 @@ PyImaging_Jpeg2KEncoderNew(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 33d62fc8a1a15f26376bfdd9fc292449a4e0827e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 23:11:59 +1100 Subject: [PATCH 08/34] Added error messages --- Tests/test_file_jpeg2k.py | 16 ++++++++++++++++ src/decode.c | 1 + src/encode.c | 3 +++ 3 files changed, 20 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index df686df32..85851ddad 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -148,6 +148,22 @@ def test_prog_res_rt(card: ImageFile.ImageFile) -> None: assert_image_equal(im, card) +def test_unknown_progression(tmp_path: Path) -> None: + outfile = tmp_path / "temp.jp2" + + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError, match="unknown progression"): + im.save(outfile, progression="invalid") + + +def test_unknown_cinema_mode(tmp_path: Path) -> None: + outfile = tmp_path / "temp.jp2" + + im = Image.new("1", (1, 1)) + with pytest.raises(ValueError, match="unknown cinema mode"): + im.save(outfile, cinema_mode="invalid") + + @pytest.mark.parametrize("num_resolutions", range(2, 6)) def test_default_num_resolutions( card: ImageFile.ImageFile, num_resolutions: int diff --git a/src/decode.c b/src/decode.c index c5c9cf56f..cda4ce702 100644 --- a/src/decode.c +++ b/src/decode.c @@ -905,6 +905,7 @@ PyImaging_Jpeg2KDecoderNew(PyObject *self, PyObject *args) { } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { + PyErr_SetString(PyExc_ValueError, "unknown codec format"); return NULL; } diff --git a/src/encode.c b/src/encode.c index b9feb28d2..b268ad741 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1346,6 +1346,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(format, "jp2") == 0) { codec_format = OPJ_CODEC_JP2; } else { + PyErr_SetString(PyExc_ValueError, "unknown codec format"); return NULL; } @@ -1360,6 +1361,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(progression, "CPRL") == 0) { prog_order = OPJ_CPRL; } else { + PyErr_SetString(PyExc_ValueError, "unknown progression"); return NULL; } @@ -1372,6 +1374,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } else if (strcmp(cinema_mode, "cinema4k-24") == 0) { cine_mode = OPJ_CINEMA4K_24; } else { + PyErr_SetString(PyExc_ValueError, "unknown cinema mode"); return NULL; } From 5b69607c35943f782cf854bb06596c34ca1a3628 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:35:27 +1100 Subject: [PATCH 09/34] Skip build 1.4.1 for lint (#9491) Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index de18946ef..37e2296fc 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ commands = [testenv:lint] skip_install = true deps = + build!=1.4.1 # pending https://github.com/pypa/build/pull/1003 check-manifest prek pass_env = From d4f78128abdc2ffd32e10f66024ee331241053ce Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:47:22 +0200 Subject: [PATCH 10/34] Revert "Skip build 1.4.1 for lint" (#9495) --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 37e2296fc..de18946ef 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,6 @@ commands = [testenv:lint] skip_install = true deps = - build!=1.4.1 # pending https://github.com/pypa/build/pull/1003 check-manifest prek pass_env = From f551ecdc430425846483b4a55bfc2956c8044b44 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 09:57:10 +1100 Subject: [PATCH 11/34] If Makernote is truncated, do not raise struct.error --- Tests/test_file_mpo.py | 35 ++++++++++- src/PIL/Image.py | 131 +++++++++++++++++++++-------------------- 2 files changed, 101 insertions(+), 65 deletions(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 4db62bd6d..7f35693f5 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -6,7 +6,14 @@ from typing import Any import pytest -from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + MpoImagePlugin, + TiffImagePlugin, + _binary, +) from .helper import ( assert_image_equal, @@ -145,6 +152,32 @@ def test_parallax() -> None: assert exif.get_ifd(0x927C)[0xB211] == -3.125 +def test_truncated_makernote() -> None: + def check(ifd: TiffImagePlugin.ImageFileDirectory_v2) -> None: + fp = BytesIO() + ifd.save(fp) + + e = Image.Exif() + e.load(fp.getvalue()) + assert e.get_ifd(37500) == {} + + # Nintendo + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[271] = "Nintendo" + ifd[34665] = {37500: b" "} + check(ifd) + + # Fujifilm + for data in ( + b"FUJIFILM", + b"FUJIFILM" + _binary.o32le(50), + b"FUJIFILM" + _binary.o32le(0), + ): + ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[34665] = {37500: data} + check(ifd) + + def test_reload_exif_after_seek() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f154cda2b..bde335504 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -4234,80 +4234,83 @@ class Exif(_ExifBase): if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 - if tag_data.startswith(b"FUJIFILM"): - ifd_offset = i32le(tag_data, 8) - ifd_data = tag_data[ifd_offset:] + try: + if tag_data.startswith(b"FUJIFILM"): + ifd_offset = i32le(tag_data, 8) + ifd_data = tag_data[ifd_offset:] - makernote = {} - for i in range(struct.unpack(" 4: - (offset,) = struct.unpack(" 4: + (offset,) = struct.unpack("H", tag_data[:2])[0]): - ifd_tag, typ, count, data = struct.unpack( - ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] - ) - if ifd_tag == 0x1101: - # CameraInfo - (offset,) = struct.unpack(">L", data) - self.fp.seek(offset) + if not data: + continue - camerainfo: dict[str, int | bytes] = { - "ModelID": self.fp.read(4) - } + makernote[ifd_tag] = handler( + ImageFileDirectory_v2(), data, False + ) + self._ifds[tag] = dict(self._fixup_dict(makernote)) + elif self.get(0x010F) == "Nintendo": + makernote = {} + for i in range(struct.unpack(">H", tag_data[:2])[0]): + ifd_tag, typ, count, data = struct.unpack( + ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] + ) + if ifd_tag == 0x1101: + # CameraInfo + (offset,) = struct.unpack(">L", data) + self.fp.seek(offset) - self.fp.read(4) - # Seconds since 2000 - camerainfo["TimeStamp"] = i32le(self.fp.read(12)) + camerainfo: dict[str, int | bytes] = { + "ModelID": self.fp.read(4) + } - self.fp.read(4) - camerainfo["InternalSerialNumber"] = self.fp.read(4) + self.fp.read(4) + # Seconds since 2000 + camerainfo["TimeStamp"] = i32le(self.fp.read(12)) - self.fp.read(12) - parallax = self.fp.read(4) - handler = ImageFileDirectory_v2._load_dispatch[ - TiffTags.FLOAT - ][1] - camerainfo["Parallax"] = handler( - ImageFileDirectory_v2(), parallax, False - )[0] + self.fp.read(4) + camerainfo["InternalSerialNumber"] = self.fp.read(4) - self.fp.read(4) - camerainfo["Category"] = self.fp.read(2) + self.fp.read(12) + parallax = self.fp.read(4) + handler = ImageFileDirectory_v2._load_dispatch[ + TiffTags.FLOAT + ][1] + camerainfo["Parallax"] = handler( + ImageFileDirectory_v2(), parallax, False + )[0] - makernote = {0x1101: camerainfo} - self._ifds[tag] = makernote + self.fp.read(4) + camerainfo["Category"] = self.fp.read(2) + + makernote = {0x1101: camerainfo} + self._ifds[tag] = makernote + except struct.error: + pass else: # Interop ifd = self._get_ifd_dict(tag_data, tag) From 67c0767b6487811f04498017f8c85fe45c510661 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 25 Mar 2026 09:53:27 +1100 Subject: [PATCH 12/34] If Photoshop blocks are truncated, do not raise struct.error --- Tests/test_file_tiff.py | 10 ++++++++++ src/PIL/TiffImagePlugin.py | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c6c8467d6..e442471d1 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -16,6 +16,7 @@ from PIL import ( TiffImagePlugin, TiffTags, UnidentifiedImageError, + _binary, ) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -941,6 +942,15 @@ class TestFileTiff: 4001, ] + def test_truncated_photoshop_blocks(self) -> None: + with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + im.tag_v2[34377] = b"8BIM" + assert im.get_photoshop_blocks() == {} + + im.tag_v2[34377] = b"8BIM" + _binary.o16be(0) + _binary.o8(2) + b" " * 5 + assert im.get_photoshop_blocks() == {} + def test_tiff_chunks(self, tmp_path: Path) -> None: tmpfile = tmp_path / "temp.tif" diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index de2ce066e..3eec94dca 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1287,10 +1287,13 @@ class TiffImageFile(ImageFile.ImageFile): blocks = {} val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: - while val.startswith(b"8BIM"): + while val.startswith(b"8BIM") and len(val) >= 12: id = i16(val[4:6]) n = math.ceil((val[6] + 1) / 2) * 2 - size = i32(val[6 + n : 10 + n]) + try: + size = i32(val[6 + n : 10 + n]) + except struct.error: + break data = val[10 + n : 10 + n + size] blocks[id] = {"data": data} From da729c832c26facdcc3afac52417cecc7641f232 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:43:32 +1100 Subject: [PATCH 13/34] Check if PyObject_CallMethod result is NULL (#9494) --- src/libImaging/SgiRleDecode.c | 13 ++++++++++--- src/libImaging/codec_fd.c | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index a562f582c..2f5268b80 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -175,8 +175,15 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t /* Get all data from File descriptor */ c = (SGISTATE *)state->context; - _imaging_seek_pyFd(state->fd, 0L, SEEK_END); + if (_imaging_seek_pyFd(state->fd, 0L, SEEK_END) == -1) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } c->bufsize = _imaging_tell_pyFd(state->fd); + if (c->bufsize == -1) { + state->errcode = IMAGING_CODEC_UNKNOWN; + return -1; + } c->bufsize -= SGI_HEADER_SIZE; c->tablen = im->bands * im->ysize; @@ -194,8 +201,8 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t state->errcode = IMAGING_CODEC_MEMORY; return -1; } - _imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET); - if (_imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { + if (_imaging_seek_pyFd(state->fd, SGI_HEADER_SIZE, SEEK_SET) == -1 || + _imaging_read_pyFd(state->fd, (char *)ptr, c->bufsize) != c->bufsize) { free(ptr); state->errcode = IMAGING_CODEC_UNKNOWN; return -1; diff --git a/src/libImaging/codec_fd.c b/src/libImaging/codec_fd.c index 526168110..dc8577298 100644 --- a/src/libImaging/codec_fd.c +++ b/src/libImaging/codec_fd.c @@ -12,6 +12,9 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { int bytes_result; result = PyObject_CallMethod(fd, "read", "n", bytes); + if (result == NULL) { + goto err; + } bytes_result = PyBytes_AsStringAndSize(result, &buffer, &length); if (bytes_result == -1) { @@ -28,7 +31,7 @@ _imaging_read_pyFd(PyObject *fd, char *dest, Py_ssize_t bytes) { return length; err: - Py_DECREF(result); + Py_XDECREF(result); return -1; } @@ -41,6 +44,10 @@ _imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) { result = PyObject_CallMethod(fd, "write", "O", byteObj); Py_DECREF(byteObj); + if (result == NULL) { + return -1; + } + Py_DECREF(result); return bytes; @@ -51,6 +58,9 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence) { PyObject *result; result = PyObject_CallMethod(fd, "seek", "ni", offset, whence); + if (result == NULL) { + return -1; + } Py_DECREF(result); return 0; @@ -62,6 +72,9 @@ _imaging_tell_pyFd(PyObject *fd) { Py_ssize_t location; result = PyObject_CallMethod(fd, "tell", NULL); + if (result == NULL) { + return -1; + } location = PyLong_AsSsize_t(result); Py_DECREF(result); From d305ee6a258a1c97b9187160553410c5610b75c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:45:02 +1100 Subject: [PATCH 14/34] Check PyType_Ready return values (#9502) --- src/_imagingcms.c | 5 +++-- src/_imagingft.c | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index ad3b27896..7db1baef0 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1447,8 +1447,9 @@ setup_module(PyObject *m) { int vn; /* Ready object types */ - PyType_Ready(&CmsProfile_Type); - PyType_Ready(&CmsTransform_Type); + if (PyType_Ready(&CmsProfile_Type) < 0 || PyType_Ready(&CmsTransform_Type) < 0) { + return -1; + } Py_INCREF(&CmsProfile_Type); PyModule_AddObject(m, "CmsProfile", (PyObject *)&CmsProfile_Type); diff --git a/src/_imagingft.c b/src/_imagingft.c index 3be1bcb9a..8395eee2c 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1545,7 +1545,9 @@ setup_module(PyObject *m) { d = PyModule_GetDict(m); /* Ready object type */ - PyType_Ready(&Font_Type); + if (PyType_Ready(&Font_Type) < 0) { + return -1; + } if (FT_Init_FreeType(&library)) { return 0; /* leave it uninitialized */ From fcecc8c6c4b16a8dbb1b02e97be1f8d2f7d60965 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:45:40 +1100 Subject: [PATCH 15/34] Fixed AVIF and WEBP dealloc (#9501) --- src/_avif.c | 8 ++++---- src/_webp.c | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/_avif.c b/src/_avif.c index 5e8b9fe8e..a9ae89f23 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -425,7 +425,7 @@ end: return (PyObject *)self; } -PyObject * +void _encoder_dealloc(AvifEncoderObject *self) { if (self->encoder) { avifEncoderDestroy(self->encoder); @@ -433,7 +433,7 @@ _encoder_dealloc(AvifEncoderObject *self) { if (self->image) { avifImageDestroy(self->image); } - Py_RETURN_NONE; + Py_TYPE(self)->tp_free(self); } PyObject * @@ -687,13 +687,13 @@ AvifDecoderNew(PyObject *self_, PyObject *args) { return (PyObject *)self; } -PyObject * +void _decoder_dealloc(AvifDecoderObject *self) { if (self->decoder) { avifDecoderDestroy(self->decoder); } PyBuffer_Release(&self->buffer); - Py_RETURN_NONE; + Py_TYPE(self)->tp_free(self); } PyObject * diff --git a/src/_webp.c b/src/_webp.c index d065e329c..ea7e77133 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -219,6 +219,7 @@ _anim_encoder_dealloc(PyObject *self) { WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPPictureFree(&(encp->frame)); WebPAnimEncoderDelete(encp->enc); + Py_TYPE(self)->tp_free(self); } PyObject * @@ -441,6 +442,7 @@ _anim_decoder_dealloc(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPDataClear(&(decp->data)); WebPAnimDecoderDelete(decp->dec); + Py_TYPE(self)->tp_free(self); } PyObject * From 92ccedea87cfbee4c40aac2a71ce3c1ede715c32 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:46:33 +1100 Subject: [PATCH 16/34] Release reference to encoder on error (#9500) --- src/encode.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/encode.c b/src/encode.c index b268ad741..1fc31404d 100644 --- a/src/encode.c +++ b/src/encode.c @@ -727,6 +727,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { const RawModeID rawmode = findRawModeID(rawmode_name); if (get_packer(encoder, mode, rawmode) < 0) { + Py_DECREF(encoder); return NULL; } @@ -742,6 +743,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { for (pos = 0; pos < tags_size; pos++) { item = PyList_GetItemRef(tags, pos); if (item == NULL) { + Py_DECREF(encoder); return NULL; } @@ -766,6 +768,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { if (!is_core_tag) { PyObject *tag_type; if (PyDict_GetItemRef(types, key, &tag_type) < 0) { + Py_DECREF(encoder); return NULL; // Exception has been already set } if (tag_type) { @@ -837,6 +840,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { if (key_int == TIFFTAG_COLORMAP) { int stride = 256; if (len != 768) { + Py_DECREF(encoder); PyErr_SetString( PyExc_ValueError, "Requiring 768 items for Colormap" ); From 9b7dccfe32dfa620afa54050e83d673903ce81e4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:47:58 +1100 Subject: [PATCH 17/34] Use PyModule_AddObjectRef (#9503) --- src/_imaging.c | 25 +++++++++++++++---------- src/_imagingcms.c | 9 ++++----- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index b8b8df5c2..697fe0ce5 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4324,8 +4324,9 @@ setup_module(PyObject *m) { #else have_libjpegturbo = Py_False; #endif - Py_INCREF(have_libjpegturbo); - PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); + if (PyModule_AddObjectRef(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo) < 0) { + return -1; + } PyObject *have_mozjpeg; #ifdef JPEG_C_PARAM_SUPPORTED @@ -4333,8 +4334,9 @@ setup_module(PyObject *m) { #else have_mozjpeg = Py_False; #endif - Py_INCREF(have_mozjpeg); - PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg); + if (PyModule_AddObjectRef(m, "HAVE_MOZJPEG", have_mozjpeg) < 0) { + return -1; + } PyObject *have_libimagequant; #ifdef HAVE_LIBIMAGEQUANT @@ -4348,8 +4350,9 @@ setup_module(PyObject *m) { #else have_libimagequant = Py_False; #endif - Py_INCREF(have_libimagequant); - PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", have_libimagequant); + if (PyModule_AddObjectRef(m, "HAVE_LIBIMAGEQUANT", have_libimagequant) < 0) { + return -1; + } #ifdef HAVE_LIBZ /* zip encoding strategies */ @@ -4377,8 +4380,9 @@ setup_module(PyObject *m) { #else have_zlibng = Py_False; #endif - Py_INCREF(have_zlibng); - PyModule_AddObject(m, "HAVE_ZLIBNG", have_zlibng); + if (PyModule_AddObjectRef(m, "HAVE_ZLIBNG", have_zlibng) < 0) { + return -1; + } #ifdef HAVE_LIBTIFF { @@ -4395,8 +4399,9 @@ setup_module(PyObject *m) { #else have_xcb = Py_False; #endif - Py_INCREF(have_xcb); - PyModule_AddObject(m, "HAVE_XCB", have_xcb); + if (PyModule_AddObjectRef(m, "HAVE_XCB", have_xcb) < 0) { + return -1; + } PyObject *pillow_version = PyUnicode_FromString(version); PyDict_SetItemString( diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 7db1baef0..7b4fb00a4 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1451,11 +1451,10 @@ setup_module(PyObject *m) { return -1; } - Py_INCREF(&CmsProfile_Type); - PyModule_AddObject(m, "CmsProfile", (PyObject *)&CmsProfile_Type); - - Py_INCREF(&CmsTransform_Type); - PyModule_AddObject(m, "CmsTransform", (PyObject *)&CmsTransform_Type); + if (PyModule_AddObjectRef(m, "CmsProfile", (PyObject *)&CmsProfile_Type) < 0 || + PyModule_AddObjectRef(m, "CmsTransform", (PyObject *)&CmsTransform_Type) < 0) { + return -1; + } d = PyModule_GetDict(m); From f176f5dad642f16663da4c587a85aa671f00abba Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 27 Mar 2026 08:57:45 +1100 Subject: [PATCH 18/34] Update libpng to 1.6.56 (#9499) --- .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 fd5b59870..107eeae9b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -92,7 +92,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds. FREETYPE_VERSION=2.14.3 HARFBUZZ_VERSION=13.2.1 -LIBPNG_VERSION=1.6.55 +LIBPNG_VERSION=1.6.56 JPEGTURBO_VERSION=3.1.3 OPENJPEG_VERSION=2.5.4 XZ_VERSION=5.8.2 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b82885e8e..d958a4592 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ V = { "LCMS2": "2.18", "LIBAVIF": "1.4.1", "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.55", + "LIBPNG": "1.6.56", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", From ef6951d1a5b7c0cb789a56cae16f06e8260a4587 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:57:43 +0200 Subject: [PATCH 19/34] CI: Retry failed downloads (#9506) --- depends/download-and-extract.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index 04bfbc755..520104753 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -5,7 +5,10 @@ archive=$1 url=$2 if [ ! -f $archive.tar.gz ]; then - wget --no-verbose -O $archive.tar.gz $url + wget -O $archive.tar.gz $url \ + --no-verbose \ + --retry-connrefused \ + --retry-on-http-error=429,503,504 fi rmdir $archive From 7672b19af4107748301187dae539c38d5126fa34 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 27 Mar 2026 04:23:01 +0000 Subject: [PATCH 20/34] Fix missing null dereference checks (#9489) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/_avif.c | 16 ++++++++++++++++ src/_imaging.c | 6 ++++++ src/_imagingft.c | 7 ++++++- src/_imagingmorph.c | 11 +++++++++++ src/_webp.c | 3 +++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/_avif.c b/src/_avif.c index a9ae89f23..1fe0cb986 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -708,15 +708,27 @@ _decoder_get_info(AvifDecoderObject *self) { if (image->xmp.size) { xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size); + if (!xmp) { + return NULL; + } } if (image->exif.size) { exif = PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); + if (!exif) { + Py_XDECREF(xmp); + return NULL; + } } if (image->icc.size) { icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); + if (!icc) { + Py_XDECREF(xmp); + Py_XDECREF(exif); + return NULL; + } } ret = Py_BuildValue( @@ -799,6 +811,7 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) { PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size"); + avifRGBImageFreePixels(&rgb); return NULL; } @@ -806,6 +819,9 @@ _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); avifRGBImageFreePixels(&rgb); + if (!bytes) { + return NULL; + } ret = Py_BuildValue( "SKKK", diff --git a/src/_imaging.c b/src/_imaging.c index 697fe0ce5..8ec9b14cd 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2473,6 +2473,9 @@ _split(ImagingObject *self) { } list = PyTuple_New(self->image->bands); + if (!list) { + return NULL; + } for (i = 0; i < self->image->bands; i++) { imaging_object = PyImagingNew(bands[i]); if (!imaging_object) { @@ -3769,6 +3772,9 @@ _ptr_destructor(PyObject *capsule) { static PyObject * _getattr_ptr(ImagingObject *self, void *closure) { PyObject *capsule = PyCapsule_New(self->image, IMAGING_MAGIC, _ptr_destructor); + if (!capsule) { + return NULL; + } Py_INCREF(self); PyCapsule_SetContext(capsule, self); return capsule; diff --git a/src/_imagingft.c b/src/_imagingft.c index 8395eee2c..1ac9d95ee 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -941,8 +941,13 @@ font_render(FontObject *self, PyObject *args) { return NULL; } PyObject *imagePtr = PyObject_GetAttrString(image, "ptr"); + if (!imagePtr) { + PyMem_Del(glyph_info); + Py_DECREF(image); + return NULL; + } im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); - Py_XDECREF(imagePtr); + Py_DECREF(imagePtr); x_offset = round(x_offset - stroke_width); y_offset = round(y_offset - stroke_width); diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 5995f9d42..c1b772760 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -143,6 +143,9 @@ match(PyObject *self, PyObject *args) { } imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!imgin) { + return NULL; + } if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); @@ -185,6 +188,10 @@ match(PyObject *self, PyObject *args) { (b6 << 6) | (b7 << 7) | (b8 << 8)); if (lut[lut_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + if (!coordObj) { + Py_DECREF(ret); + return NULL; + } PyList_Append(ret, coordObj); Py_XDECREF(coordObj); } @@ -230,6 +237,10 @@ get_on_pixels(PyObject *self, PyObject *args) { for (col_idx = 0; col_idx < width; col_idx++) { if (row[col_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); + if (!coordObj) { + Py_DECREF(ret); + return NULL; + } PyList_Append(ret, coordObj); Py_XDECREF(coordObj); } diff --git a/src/_webp.c b/src/_webp.c index ea7e77133..a936e13d8 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -505,6 +505,9 @@ _anim_decoder_get_next(PyObject *self) { bytes = PyBytes_FromStringAndSize( (char *)buf, decp->info.canvas_width * 4 * decp->info.canvas_height ); + if (!bytes) { + return NULL; + } ret = Py_BuildValue("Si", bytes, timestamp); From 40400edd6229e9c26c22b0d3d4bcfa1cc7ec5cb0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Mar 2026 15:26:25 +1100 Subject: [PATCH 21/34] Check PyCapsule_GetPointer return value --- src/_imaging.c | 6 ++++++ src/_imagingcms.c | 6 ++++++ src/_imagingft.c | 5 +++++ src/_imagingmath.c | 21 +++++++++++++++++++++ src/_imagingmorph.c | 9 +++++++++ src/_webp.c | 6 ++++++ src/libImaging/Storage.c | 6 ++++++ 7 files changed, 59 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index 8ec9b14cd..ac0317f78 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -254,6 +254,9 @@ void ReleaseArrowSchemaPyCapsule(PyObject *capsule) { struct ArrowSchema *schema = (struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema"); + if (!schema) { + return; + } if (schema->release != NULL) { schema->release(schema); } @@ -276,6 +279,9 @@ void ReleaseArrowArrayPyCapsule(PyObject *capsule) { struct ArrowArray *array = (struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array"); + if (!array) { + return; + } if (array->release != NULL) { array->release(array); } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 7b4fb00a4..469be05f4 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -558,7 +558,13 @@ cms_transform_apply(CmsTransformObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } imOut = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!imOut) { + return NULL; + } return Py_BuildValue("i", pyCMSdoTransform(im, imOut, self->transform)); } diff --git a/src/_imagingft.c b/src/_imagingft.c index 1ac9d95ee..5d91eaad6 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -948,6 +948,11 @@ font_render(FontObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); Py_DECREF(imagePtr); + if (!im) { + PyMem_Del(glyph_info); + Py_DECREF(image); + return NULL; + } x_offset = round(x_offset - stroke_width); y_offset = round(y_offset - stroke_width); diff --git a/src/_imagingmath.c b/src/_imagingmath.c index 48c395900..c04361468 100644 --- a/src/_imagingmath.c +++ b/src/_imagingmath.c @@ -187,8 +187,17 @@ _unop(PyObject *self, PyObject *args) { } unop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_UNOP_MAGIC); + if (!unop) { + return NULL; + } out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!out) { + return NULL; + } im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!im1) { + return NULL; + } unop(out, im1); @@ -219,9 +228,21 @@ _binop(PyObject *self, PyObject *args) { } binop = (void *)PyCapsule_GetPointer(op, MATH_FUNC_BINOP_MAGIC); + if (!binop) { + return NULL; + } out = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!out) { + return NULL; + } im1 = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!im1) { + return NULL; + } im2 = (Imaging)PyCapsule_GetPointer(i2, IMAGING_MAGIC); + if (!im2) { + return NULL; + } binop(out, im1, im2); diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c1b772760..b6f307c84 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -53,7 +53,13 @@ apply(PyObject *self, PyObject *args) { } imgin = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!imgin) { + return NULL; + } imgout = (Imaging)PyCapsule_GetPointer(i1, IMAGING_MAGIC); + if (!imgout) { + return NULL; + } width = imgin->xsize; height = imgin->ysize; @@ -223,6 +229,9 @@ get_on_pixels(PyObject *self, PyObject *args) { } img = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!img) { + return NULL; + } rows = img->image8; width = img->xsize; height = img->ysize; diff --git a/src/_webp.c b/src/_webp.c index a936e13d8..115141273 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -262,6 +262,9 @@ _anim_encoder_add(PyObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } // Setup config for this frame if (!WebPConfigInit(&config)) { @@ -610,6 +613,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { } im = (Imaging)PyCapsule_GetPointer(i0, IMAGING_MAGIC); + if (!im) { + return NULL; + } // Setup config for this frame if (!WebPConfigInit(&config)) { diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index c09062c92..c8175612e 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -677,9 +677,15 @@ ImagingNewArrow( Imaging im; struct ArrowSchema *schema = (struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema"); + if (!schema) { + return NULL; + } struct ArrowArray *external_array = (struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array"); + if (!external_array) { + return NULL; + } if (xsize < 0 || ysize < 0) { return (Imaging)ImagingError_ValueError("bad image size"); From 20a9401971c46961f49091979b7c65dac0fae202 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Mar 2026 21:21:22 +1100 Subject: [PATCH 22/34] Check PyBytes_FromStringAndSize return value --- src/display.c | 3 +++ src/libImaging/codec_fd.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/display.c b/src/display.c index 5b5853a3c..944c60b70 100644 --- a/src/display.c +++ b/src/display.c @@ -480,6 +480,9 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { GlobalUnlock(handle); CloseClipboard(); + if (!result) { + return NULL; + } return Py_BuildValue("zN", format_names[format], result); } diff --git a/src/libImaging/codec_fd.c b/src/libImaging/codec_fd.c index dc8577298..c5614e603 100644 --- a/src/libImaging/codec_fd.c +++ b/src/libImaging/codec_fd.c @@ -41,6 +41,9 @@ _imaging_write_pyFd(PyObject *fd, char *src, Py_ssize_t bytes) { PyObject *byteObj; byteObj = PyBytes_FromStringAndSize(src, bytes); + if (!byteObj) { + return -1; + } result = PyObject_CallMethod(fd, "write", "O", byteObj); Py_DECREF(byteObj); From 27de86483d8c23d9375b071993c04c2ff8388ca3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Mar 2026 21:54:45 +1100 Subject: [PATCH 23/34] Switch iOS back to macos-15-intel --- .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 ed49f15c0..af2f9b3e8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -104,7 +104,7 @@ jobs: cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios - os: macos-26-intel + os: macos-15-intel cibw_arch: x86_64_iphonesimulator steps: - uses: actions/checkout@v6 From b337b33564da0b21d244b46c2b3e954ae6afc099 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:45:59 +0200 Subject: [PATCH 24/34] PERF101 --- pyproject.toml | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6d9910ca1..4190f091a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,21 +139,22 @@ exclude = "wheels/multibuild" exclude = [ "wheels/multibuild" ] fix = true lint.select = [ - "C4", # flake8-comprehensions - "E", # pycodestyle errors - "EM", # flake8-errmsg - "F", # pyflakes errors - "I", # isort - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PT", # flake8-pytest-style - "PYI", # flake8-pyi - "RUF100", # unused noqa (yesqa) - "UP", # pyupgrade - "W", # pycodestyle warnings - "YTT", # flake8-2020 + "C4", # flake8-comprehensions + "E", # pycodestyle errors + "EM", # flake8-errmsg + "F", # pyflakes errors + "I", # isort + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PERF101", # perflint: unnecessary-list-cast + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "RUF100", # unused noqa (yesqa) + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 ] lint.ignore = [ "E203", # Whitespace before ':' From 624fc87d2d91f9bd763f3cd721f999f58a6752bb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:46:16 +0200 Subject: [PATCH 25/34] PERF102 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4190f091a..7178cb2de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,6 +147,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PERF101", # perflint: unnecessary-list-cast + "PERF102", # perflint: incorrect-dict-iterator "PGH", # pygrep-hooks "PIE", # flake8-pie "PT", # flake8-pytest-style From b85b8534d7ca1132cfe4067ae2963c26f07b7811 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:11:12 +0200 Subject: [PATCH 26/34] PERF401 and fixes --- pyproject.toml | 1 + setup.py | 8 ++++---- src/PIL/IcnsImagePlugin.py | 3 +-- src/PIL/ImageDraw.py | 4 +--- src/PIL/ImageFile.py | 6 ++++-- winbuild/build_prepare.py | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7178cb2de..ea43e9cb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,6 +148,7 @@ lint.select = [ "LOG", # flake8-logging "PERF101", # perflint: unnecessary-list-cast "PERF102", # perflint: incorrect-dict-iterator + "PERF401", # perflint: manual-list-comprehension "PGH", # pygrep-hooks "PIE", # flake8-pie "PT", # flake8-pytest-style diff --git a/setup.py b/setup.py index 3d975950b..175aed25a 100644 --- a/setup.py +++ b/setup.py @@ -1078,10 +1078,10 @@ libraries: list[tuple[str, _BuildInfo]] = [ ] files: list[str | os.PathLike[str]] = ["src/_imaging.c"] -for src_file in _IMAGING: - files.append("src/" + src_file + ".c") -for src_file in _LIB_IMAGING: - files.append(os.path.join("src/libImaging", src_file + ".c")) +files.extend("src/" + src_file + ".c" for src_file in _IMAGING) +files.extend( + os.path.join("src/libImaging", src_file + ".c") for src_file in _LIB_IMAGING +) ext_modules = [ Extension("PIL._imaging", files), Extension("PIL._imagingft", ["src/_imagingft.c"]), diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 023835fb7..cb7a74c2e 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -80,8 +80,7 @@ def read_32( if byte_int & 0x80: blocksize = byte_int - 125 byte = fobj.read(1) - for i in range(blocksize): - data.append(byte) + data.extend([byte] * blocksize) else: blocksize = byte_int + 1 data.append(fobj.read(blocksize)) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index eb108ac41..506bb3b43 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -597,9 +597,7 @@ class ImageDraw: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" - coord = [] - for i in range(2): - coord.append(int(xy[i])) + coord = [int(xy[i]) for i in range(2)] start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) try: mask, offset = image_text.font.getmask2( # type: ignore[union-attr,misc] diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 50e0075a2..df2a82b73 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -215,8 +215,10 @@ class ImageFile(Image.Image): if subifd_offsets: if not isinstance(subifd_offsets, tuple): subifd_offsets = (subifd_offsets,) - for subifd_offset in subifd_offsets: - ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifds = [ + (exif._get_ifd_dict(subifd_offset), subifd_offset) + for subifd_offset in subifd_offsets + ] ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): assert exif._info is not None diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d958a4592..1438827ca 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -544,11 +544,11 @@ def write_script( def get_footer(dep: dict[str, Any]) -> list[str]: lines = [] for out in dep.get("headers", []): - lines.append(cmd_copy(out, "{inc_dir}")) + lines.append(cmd_copy(out, "{inc_dir}")) # noqa: PERF401 for out in dep.get("libs", []): - lines.append(cmd_copy(out, "{lib_dir}")) + lines.append(cmd_copy(out, "{lib_dir}")) # noqa: PERF401 for out in dep.get("bins", []): - lines.append(cmd_copy(out, "{bin_dir}")) + lines.append(cmd_copy(out, "{bin_dir}")) # noqa: PERF401 return lines From 9a358fa289e87b849d08ead61a6dccabf5961121 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:12:32 +0200 Subject: [PATCH 27/34] PERF402 and fixes --- Tests/test_file_container.py | 4 +--- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 597ab5083..c73f2a40c 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -179,9 +179,7 @@ def test_iter(bytesmode: bool) -> None: container = ContainerIO.ContainerIO(fh, 0, 120) # Act - data = [] - for line in container: - data.append(line) + data = list(container) # Assert if bytesmode: diff --git a/pyproject.toml b/pyproject.toml index ea43e9cb1..149dbbac5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,6 +149,7 @@ lint.select = [ "PERF101", # perflint: unnecessary-list-cast "PERF102", # perflint: incorrect-dict-iterator "PERF401", # perflint: manual-list-comprehension + "PERF402", # perflint: manual-list-copy "PGH", # pygrep-hooks "PIE", # flake8-pie "PT", # flake8-pytest-style From 090ca9461b1ce0e0f91644f6cc7a1d1416e7b915 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:20:06 +0200 Subject: [PATCH 28/34] PERF403 and fixes --- pyproject.toml | 1 + src/PIL/IptcImagePlugin.py | 11 ++--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 149dbbac5..bda99c3bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,6 +150,7 @@ lint.select = [ "PERF102", # perflint: incorrect-dict-iterator "PERF401", # perflint: manual-list-comprehension "PERF402", # perflint: manual-list-copy + "PERF403", # perflint: manual-dict-comprehension "PGH", # pygrep-hooks "PIE", # flake8-pie "PT", # flake8-pytest-style diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 6fc824e4c..9c8be8b4e 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -185,13 +185,9 @@ def getiptcinfo( data = None - info: dict[tuple[int, int], bytes | list[bytes]] = {} if isinstance(im, IptcImageFile): # return info dictionary right away - for k, v in im.info.items(): - if isinstance(k, tuple): - info[k] = v - return info + return {k: v for k, v in im.info.items() if isinstance(k, tuple)} elif isinstance(im, JpegImagePlugin.JpegImageFile): # extract the IPTC/NAA resource @@ -227,7 +223,4 @@ def getiptcinfo( except (IndexError, KeyError): pass # expected failure - for k, v in iptc_im.info.items(): - if isinstance(k, tuple): - info[k] = v - return info + return {k: v for k, v in iptc_im.info.items() if isinstance(k, tuple)} From 754c7ea3a0aa47429809a1675f249263de3eac7b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:28:11 +0200 Subject: [PATCH 29/34] PERF203 and fixes --- Tests/test_bmp_reference.py | 4 ++-- Tests/test_file_libtiff.py | 5 +---- pyproject.toml | 36 ++++++++++++++++-------------------- setup.py | 2 +- src/PIL/GifImagePlugin.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/ImageFont.py | 2 +- src/PIL/ImagePalette.py | 6 ++---- src/PIL/JpegImagePlugin.py | 4 ++-- src/PIL/PcfFontFile.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- 11 files changed, 29 insertions(+), 38 deletions(-) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 8fbd73748..ea0853100 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -56,7 +56,7 @@ def test_questionable() -> None: im.load() if os.path.basename(f) not in supported: print(f"Please add {f} to the partially supported bmp specs.") - except Exception: # as msg: + except Exception: # noqa: PERF203 if os.path.basename(f) in supported: raise @@ -106,7 +106,7 @@ def test_good() -> None: assert_image_similar(im_converted, compare_converted, 5) - except Exception as msg: + except Exception as msg: # noqa: PERF203 # there are three here that are unsupported: unsupported = ( os.path.join(base, "g", "rgb32bf.bmp"), diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index b453e3aa5..6f20900e4 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -224,10 +224,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/hopper_g4.tif") as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) for tag in im.tag_v2: - try: - del core_items[tag] - except KeyError: - pass + core_items.pop(tag, None) del core_items[320] # colormap is special, tested below # Type codes: diff --git a/pyproject.toml b/pyproject.toml index bda99c3bf..7eb9a3fbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,26 +139,22 @@ exclude = "wheels/multibuild" exclude = [ "wheels/multibuild" ] fix = true lint.select = [ - "C4", # flake8-comprehensions - "E", # pycodestyle errors - "EM", # flake8-errmsg - "F", # pyflakes errors - "I", # isort - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PERF101", # perflint: unnecessary-list-cast - "PERF102", # perflint: incorrect-dict-iterator - "PERF401", # perflint: manual-list-comprehension - "PERF402", # perflint: manual-list-copy - "PERF403", # perflint: manual-dict-comprehension - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PT", # flake8-pytest-style - "PYI", # flake8-pyi - "RUF100", # unused noqa (yesqa) - "UP", # pyupgrade - "W", # pycodestyle warnings - "YTT", # flake8-2020 + "C4", # flake8-comprehensions + "E", # pycodestyle errors + "EM", # flake8-errmsg + "F", # pyflakes errors + "I", # isort + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PERF", # perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "PYI", # flake8-pyi + "RUF100", # unused noqa (yesqa) + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 ] lint.ignore = [ "E203", # Whitespace before ':' diff --git a/setup.py b/setup.py index 175aed25a..496c8cb1f 100644 --- a/setup.py +++ b/setup.py @@ -302,7 +302,7 @@ def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: subprocess.check_output(command_cflags).decode("utf8").strip(), )[::2][1:] return libs, cflags - except Exception: + except Exception: # noqa: PERF203 pass return None diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 390b3b374..1ffb18b9c 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -167,7 +167,7 @@ class GifImageFile(ImageFile.ImageFile): for f in range(self.__frame + 1, frame + 1): try: self._seek(f) - except EOFError as e: + except EOFError as e: # noqa: PERF203 self.seek(last_frame) msg = "no more images in GIF file" raise EOFError(msg) from e diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bde335504..6062857da 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -488,7 +488,7 @@ def init() -> bool: try: logger.debug("Importing %s", plugin) __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) - except ImportError as e: + except ImportError as e: # noqa: PERF203 logger.debug("Image: failed to import %s: %s", plugin, e) if OPEN or SAVE: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea7f4dc54..ec7c7cb08 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -930,7 +930,7 @@ def load_path(filename: str | bytes) -> ImageFont: for directory in sys.path: try: return load(os.path.join(directory, filename)) - except OSError: + except OSError: # noqa: PERF203 pass msg = f'cannot find font file "{filename}" in sys.path' if os.path.exists(filename): diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 99ad2771b..2abbd46ea 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -198,13 +198,11 @@ class ImagePalette: try: fp.write("# Palette\n") fp.write(f"# Mode: {self.mode}\n") + palette_len = len(self.palette) for i in range(256): fp.write(f"{i}") for j in range(i * len(self.mode), (i + 1) * len(self.mode)): - try: - fp.write(f" {self.palette[j]}") - except IndexError: - fp.write(" 0") + fp.write(f" {self.palette[j] if j < palette_len else 0}") fp.write("\n") finally: if open_fp: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 2f11cbfe3..d5b67bba5 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -153,7 +153,7 @@ def APP(self: JpegImageFile, marker: int) -> None: photoshop[code] = data offset += size offset += offset & 1 # align - except struct.error: + except struct.error: # noqa: PERF203 break # insufficient data elif marker == 0xFFEE and s.startswith(b"Adobe"): @@ -744,7 +744,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: msg = "Invalid quantization table" raise TypeError(msg) table_array = array.array("H", table) - except TypeError as e: + except TypeError as e: # noqa: PERF203 msg = "Invalid quantization table" raise ValueError(msg) from e else: diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index a00e9b919..b923293b0 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -251,7 +251,7 @@ class PcfFontFile(FontFile.FontFile): ] if encoding_offset != 0xFFFF: encoding[i] = encoding_offset - except UnicodeDecodeError: + except UnicodeDecodeError: # noqa: PERF203 # character is not supported in selected encoding pass diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4e082a293..d58426c55 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -869,7 +869,7 @@ class PngImageFile(ImageFile.ImageFile): for f in range(self.__frame + 1, frame + 1): try: self._seek(f) - except EOFError as e: + except EOFError as e: # noqa: PERF203 self.seek(last_frame) msg = "no more images in APNG file" raise EOFError(msg) from e From 9a7b91e5dbb6630ea4e3d5d6eccbf48f4463eda4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 15:11:04 +1100 Subject: [PATCH 30/34] PERF203 fixes --- src/PIL/GifImagePlugin.py | 12 ++++++------ src/PIL/JpegImagePlugin.py | 22 ++++++++++------------ src/PIL/PngImagePlugin.py | 12 ++++++------ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 1ffb18b9c..b8db5d832 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -164,13 +164,13 @@ class GifImageFile(ImageFile.ImageFile): self._seek(0) last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: + try: + for f in range(self.__frame + 1, frame + 1): self._seek(f) - except EOFError as e: # noqa: PERF203 - self.seek(last_frame) - msg = "no more images in GIF file" - raise EOFError(msg) from e + except EOFError as e: + self.seek(last_frame) + msg = "no more images in GIF file" + raise EOFError(msg) from e def _seek(self, frame: int, update_image: bool = True) -> None: if isinstance(self._fp, DeferredError): diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d5b67bba5..46320eb3b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -127,8 +127,8 @@ def APP(self: JpegImageFile, marker: int) -> None: # parse the image resource block offset = 14 photoshop = self.info.setdefault("photoshop", {}) - while s[offset : offset + 4] == b"8BIM": - try: + try: + while s[offset : offset + 4] == b"8BIM": offset += 4 # resource code code = i16(s, offset) @@ -153,8 +153,8 @@ def APP(self: JpegImageFile, marker: int) -> None: photoshop[code] = data offset += size offset += offset & 1 # align - except struct.error: # noqa: PERF203 - break # insufficient data + except struct.error: + pass # insufficient data elif marker == 0xFFEE and s.startswith(b"Adobe"): self.info["adobe"] = i16(s, 5) @@ -738,17 +738,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if not (0 < len(qtables) < 5): msg = "None or too many quantization tables" raise ValueError(msg) - for idx, table in enumerate(qtables): - try: + try: + for idx, table in enumerate(qtables): if len(table) != 64: msg = "Invalid quantization table" raise TypeError(msg) - table_array = array.array("H", table) - except TypeError as e: # noqa: PERF203 - msg = "Invalid quantization table" - raise ValueError(msg) from e - else: - qtables[idx] = list(table_array) + qtables[idx] = list(array.array("H", table)) + except TypeError as e: + msg = "Invalid quantization table" + raise ValueError(msg) from e return qtables if qtables == "keep": diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index d58426c55..76a15bd0d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -866,13 +866,13 @@ class PngImageFile(ImageFile.ImageFile): self._seek(0, True) last_frame = self.__frame - for f in range(self.__frame + 1, frame + 1): - try: + try: + for f in range(self.__frame + 1, frame + 1): self._seek(f) - except EOFError as e: # noqa: PERF203 - self.seek(last_frame) - msg = "no more images in APNG file" - raise EOFError(msg) from e + except EOFError as e: + self.seek(last_frame) + msg = "no more images in APNG file" + raise EOFError(msg) from e def _seek(self, frame: int, rewind: bool = False) -> None: assert self.png is not None From 701b49adc5d3f64dfb9eb2c42f0411b3d8490fef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 15:13:26 +1100 Subject: [PATCH 31/34] PERF401 fix --- winbuild/build_prepare.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1438827ca..466cca176 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -542,14 +542,11 @@ def write_script( def get_footer(dep: dict[str, Any]) -> list[str]: - lines = [] - for out in dep.get("headers", []): - lines.append(cmd_copy(out, "{inc_dir}")) # noqa: PERF401 - for out in dep.get("libs", []): - lines.append(cmd_copy(out, "{lib_dir}")) # noqa: PERF401 - for out in dep.get("bins", []): - lines.append(cmd_copy(out, "{bin_dir}")) # noqa: PERF401 - return lines + return ( + [cmd_copy(out, "{inc_dir}") for out in dep.get("headers", [])] + + [cmd_copy(out, "{lib_dir}") for out in dep.get("libs", [])] + + [cmd_copy(out, "{bin_dir}") for out in dep.get("bins", [])] + ) def build_env(prefs: dict[str, str], verbose: bool) -> None: From 1ed39726c57cd2d094f9a9a90be08814678f6190 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Mar 2026 13:23:30 +1100 Subject: [PATCH 32/34] Added release notes for #9419 --- docs/releasenotes/12.2.0.rst | 63 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 64 insertions(+) create mode 100644 docs/releasenotes/12.2.0.rst diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst new file mode 100644 index 000000000..aa1206cd0 --- /dev/null +++ b/docs/releasenotes/12.2.0.rst @@ -0,0 +1,63 @@ +12.2.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +FontFile.to_imagefont() +^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.FontFile.FontFile` instances can now be directly converted to +:py:class:`~PIL.ImageFont.ImageFont` instances:: + + >>> from PIL import PcfFontFile + >>> with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: + ... pcffont = PcfFontFile.PcfFontFile(fp) + ... pcffont.to_imagefont() + ... + + +Other changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 690be2072..076872979 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -15,6 +15,7 @@ expected to be backported to earlier versions. :maxdepth: 2 versioning + 12.2.0 12.1.1 12.1.0 12.0.0 From ccf9863ba8595b5920ee3883ffc9aba01e92ff7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Mar 2026 13:23:44 +1100 Subject: [PATCH 33/34] Added release notes for #9394 --- docs/releasenotes/12.2.0.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index aa1206cd0..0bbb9b824 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -33,10 +33,14 @@ TODO API changes =========== -TODO -^^^^ +Error when encoding an empty image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Attempting to encode an image with zero width or height would previously raise +a :py:exc:`SystemError`. That has now been changed to a :py:exc:`ValueError`. + +This does not add any new errors. SGI, ICNS and ICO formats are still able to +save (0, 0) images. API additions ============= From 3121c77cad919703c2d5c77116a03422ba744d7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Mar 2026 19:19:48 +1100 Subject: [PATCH 34/34] Added release notes for #9456 --- docs/releasenotes/12.2.0.rst | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/docs/releasenotes/12.2.0.rst b/docs/releasenotes/12.2.0.rst index 0bbb9b824..66526592a 100644 --- a/docs/releasenotes/12.2.0.rst +++ b/docs/releasenotes/12.2.0.rst @@ -14,22 +14,6 @@ TODO TODO -Backwards incompatible changes -============================== - -TODO -^^^^ - -TODO - -Deprecations -============ - -TODO -^^^^ - -TODO - API changes =========== @@ -61,7 +45,8 @@ FontFile.to_imagefont() Other changes ============= -TODO -^^^^ +Support reading JPEG2000 images with CMYK palettes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +JPEG2000 images with CMYK palettes can now be read. This is the first integration of +CMYK palettes into Pillow.