From 657d0414f07d37ec8abecc02879154834bf7a009 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 12 Feb 2026 21:51:01 +1100 Subject: [PATCH 01/18] Merge PFM into PPM --- docs/handbook/image-file-formats.rst | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 35ec99ece..add42a3a0 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -828,16 +828,6 @@ PCX Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data. -PFM -^^^ - -.. versionadded:: 10.3.0 - -Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files -containing ``F`` data. - -Color (PF format) PFM files are not supported. - Opening ~~~~~~~ @@ -1081,13 +1071,18 @@ following parameters can also be set: PPM ^^^ -Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or -``RGB`` data. +Pillow reads and writes PBM, PGM, PPM, PNM and PFM files containing ``1``, ``L``, ``I``, +``RGB`` or ``F`` data. "Raw" (P4 to P6) formats can be read, and are used when writing. Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. +Since Pillow 10.3.0, grayscale (Pf format) Portable FloatMap (PFM) files containing +``F`` data can be read and used when writing as well. + +Color (PF format) PFM files are not supported. + QOI ^^^ From f71d74eec2300c42578632f6cb83d3bf3e9dbebb Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 13 Feb 2026 18:29:41 +1100 Subject: [PATCH 02/18] Use versionadded Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/handbook/image-file-formats.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index add42a3a0..a9fd764e6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1076,10 +1076,12 @@ Pillow reads and writes PBM, PGM, PPM, PNM and PFM files containing ``1``, ``L`` "Raw" (P4 to P6) formats can be read, and are used when writing. -Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. +.. versionadded:: 9.2.0 + "Plain" (P1 to P3) formats can be read. -Since Pillow 10.3.0, grayscale (Pf format) Portable FloatMap (PFM) files containing -``F`` data can be read and used when writing as well. +.. versionadded:: 10.3.0 + Grayscale (Pf format) Portable FloatMap (PFM) files containing + ``F`` data can be read and used when writing. Color (PF format) PFM files are not supported. From 2c00c6f80e0b268f1ecc4287553e4d96084d996a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:29:42 +0200 Subject: [PATCH 03/18] GHA: Cache libavif and webp builds for Ubuntu (#9437) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> Co-authored-by: Andrew Murray --- .ci/install.sh | 2 +- .github/workflows/docs.yml | 16 +++++ .github/workflows/test.yml | 18 +++++ depends/install_libavif.sh | 138 +++++++++++++++++++++---------------- depends/install_webp.sh | 28 ++++++-- 5 files changed, 139 insertions(+), 63 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index aeb5e6514..9553eb8f4 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -53,7 +53,7 @@ pushd depends && ./install_imagequant.sh && popd pushd depends && sudo ./install_raqm.sh && popd # libavif -pushd depends && sudo ./install_libavif.sh && popd +pushd depends && ./install_libavif.sh && popd # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 44af3e3df..857881c01 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -48,6 +48,13 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libavif + uses: actions/cache@v5 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} + - name: Cache libimagequant uses: actions/cache@v5 id: cache-libimagequant @@ -55,12 +62,21 @@ jobs: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Cache libwebp + uses: actions/cache@v5 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + - name: Install Linux dependencies run: | .ci/install.sh env: GHA_PYTHON_VERSION: "3.x" + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - name: Build run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a206e269..471cab90e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,6 +91,14 @@ jobs: - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libavif + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v5 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }} + - name: Cache libimagequant if: startsWith(matrix.os, 'ubuntu') uses: actions/cache@v5 @@ -99,13 +107,23 @@ jobs: path: ~/cache-libimagequant key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Cache libwebp + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v5 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + - name: Install Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: | .ci/install.sh env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - name: Install macOS dependencies if: startsWith(matrix.os, 'macOS') diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh index a6686f3ef..0089bf2b5 100755 --- a/depends/install_libavif.sh +++ b/depends/install_libavif.sh @@ -3,66 +3,88 @@ set -eo pipefail version=1.3.0 -./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz +if [[ "$GHA_LIBAVIF_CACHE_HIT" == "true" ]]; then -pushd libavif-$version + LIBDIR=/usr/lib/x86_64-linux-gnu -# Apply patch for SVT-AV1 4.0 compatibility -# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971 -patch -p1 < ../libavif-svt4.patch + # Copy cached files into place + sudo cp ~/cache-libavif/lib/* $LIBDIR/ + sudo cp -r ~/cache-libavif/include/avif /usr/include/ -if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then - PREFIX=$(brew --prefix) else - PREFIX=/usr + + ./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz + + pushd libavif-$version + + # Apply patch for SVT-AV1 4.0 compatibility + # Pending release of https://github.com/AOMediaCodec/libavif/pull/2971 + patch -p1 < ../libavif-svt4.patch + + if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then + PREFIX=$(brew --prefix) + else + PREFIX=/usr + fi + + PKGCONFIG=${PKGCONFIG:-pkg-config} + + LIBAVIF_CMAKE_FLAGS=() + HAS_DECODER=0 + HAS_ENCODER=0 + + if $PKGCONFIG --exists aom; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) + HAS_ENCODER=1 + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists dav1d; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists libgav1; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) + HAS_DECODER=1 + fi + + if $PKGCONFIG --exists rav1e; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) + HAS_ENCODER=1 + fi + + if $PKGCONFIG --exists SvtAv1Enc; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) + HAS_ENCODER=1 + fi + + if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then + LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) + fi + + cmake \ + -DCMAKE_INSTALL_PREFIX=$PREFIX \ + -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MACOSX_RPATH=OFF \ + -DAVIF_LIBSHARPYUV=LOCAL \ + -DAVIF_LIBYUV=LOCAL \ + "${LIBAVIF_CMAKE_FLAGS[@]}" \ + . + + sudo make install + + if [ -n "$GITHUB_ACTIONS" ] && [ "$(uname)" != "Darwin" ]; then + # Copy to cache + LIBDIR=/usr/lib/x86_64-linux-gnu + rm -rf ~/cache-libavif + mkdir -p ~/cache-libavif/lib + mkdir -p ~/cache-libavif/include + cp $LIBDIR/libavif.so* ~/cache-libavif/lib/ + cp -r /usr/include/avif ~/cache-libavif/include/ + fi + + popd + fi - -PKGCONFIG=${PKGCONFIG:-pkg-config} - -LIBAVIF_CMAKE_FLAGS=() -HAS_DECODER=0 -HAS_ENCODER=0 - -if $PKGCONFIG --exists aom; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) - HAS_ENCODER=1 - HAS_DECODER=1 -fi - -if $PKGCONFIG --exists dav1d; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) - HAS_DECODER=1 -fi - -if $PKGCONFIG --exists libgav1; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) - HAS_DECODER=1 -fi - -if $PKGCONFIG --exists rav1e; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) - HAS_ENCODER=1 -fi - -if $PKGCONFIG --exists SvtAv1Enc; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) - HAS_ENCODER=1 -fi - -if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then - LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) -fi - -cmake \ - -DCMAKE_INSTALL_PREFIX=$PREFIX \ - -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_MACOSX_RPATH=OFF \ - -DAVIF_LIBSHARPYUV=LOCAL \ - -DAVIF_LIBYUV=LOCAL \ - "${LIBAVIF_CMAKE_FLAGS[@]}" \ - . - -make install - -popd diff --git a/depends/install_webp.sh b/depends/install_webp.sh index d7f3cd2f5..c328fe2c8 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -3,10 +3,30 @@ archive=libwebp-1.6.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz +if [[ "$GHA_LIBWEBP_CACHE_HIT" == "true" ]]; then -pushd $archive + # Copy cached files into place + sudo cp ~/cache-libwebp/lib/* /usr/lib/ + sudo cp -r ~/cache-libwebp/include/webp /usr/include/ -./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install +else -popd + ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + + pushd $archive + + ./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install + + if [ -n "$GITHUB_ACTIONS" ]; then + # Copy to cache + rm -rf ~/cache-libwebp + mkdir -p ~/cache-libwebp/lib + mkdir -p ~/cache-libwebp/include + cp /usr/lib/libwebp*.so* /usr/lib/libwebp*.a ~/cache-libwebp/lib/ + cp /usr/lib/libsharpyuv.so* /usr/lib/libsharpyuv.a ~/cache-libwebp/lib/ + cp -r /usr/include/webp ~/cache-libwebp/include/ + fi + + popd + +fi From a5c9eba30a4d2463cc0e9de67ae886b318af1fd2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:57:27 +1100 Subject: [PATCH 04/18] Fix unexpected error when saving zero dimension images (#9391) Co-authored-by: Andrew Murray --- Tests/test_file_gif.py | 8 ++++++++ Tests/test_file_pcx.py | 8 ++++++++ Tests/test_file_spider.py | 8 ++++++++ src/PIL/GifImagePlugin.py | 8 +++++++- src/PIL/PcxImagePlugin.py | 4 ++++ src/PIL/SpiderImagePlugin.py | 2 +- 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index e3fcec490..b52816fdc 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -310,6 +310,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: assert reloaded.getpixel((0, 0)) == 255 +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = BytesIO() + im = Image.new("RGB", size) + with pytest.raises(SystemError): + im.save(b, "GIF") + + @pytest.mark.parametrize( "path, mode", ( diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 90740ab57..509d93469 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -37,6 +37,14 @@ def test_sanity(tmp_path: Path) -> None: im.save(f) +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = io.BytesIO() + im = Image.new("1", size) + with pytest.raises(ValueError): + im.save(b, "PCX") + + def test_p_4_planes() -> None: with Image.open("Tests/images/p_4_planes.pcx") as im: assert im.getpixel((0, 0)) == 3 diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 03494523b..903632cff 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -68,6 +68,14 @@ def test_save(tmp_path: Path) -> None: assert im2.format == "SPIDER" +@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0))) +def test_save_zero(size: tuple[int, int]) -> None: + b = BytesIO() + im = Image.new("1", size) + with pytest.raises(SystemError): + im.save(b, "SPIDER") + + def test_tempfile() -> None: # Arrange im = hopper() diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 76a0d4ab9..390b3b374 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -937,7 +937,13 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: :param info: encoderinfo :returns: list of indexes of palette entries in use, or None """ - if im.mode in ("P", "L") and info and info.get("optimize"): + if ( + im.mode in ("P", "L") + and info + and info.get("optimize") + and im.width != 0 + and im.height != 0 + ): # Potentially expensive operation. # The palette saves 3 bytes per color not used, but palette diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 6b16d5385..3e34e3c63 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -146,6 +146,10 @@ SAVE = { def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + if im.width == 0 or im.height == 0: + msg = "Cannot write empty image as PCX" + raise ValueError(msg) + try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 848dccda5..11d90699d 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -244,7 +244,7 @@ def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | No def makeSpiderHeader(im: Image.Image) -> list[bytes]: nsam, nrow = im.size - lenbyt = nsam * 4 # There are labrec records in the header + lenbyt = max(1, nsam) * 4 # There are labrec records in the header labrec = int(1024 / lenbyt) if 1024 % lenbyt != 0: labrec += 1 From 3cd69cb12f10d18a58c94dc01b54c70deba19289 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:57:29 +1100 Subject: [PATCH 05/18] Specify platform when pulling docker image (#9440) Co-authored-by: Andrew Murray --- .github/workflows/test-docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 091edb222..8f24bef3d 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -83,7 +83,7 @@ jobs: - name: Docker pull run: | - docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} - name: Docker build run: | From 02764a0077d0c267fafd41e63c31beee1e5b6a7e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:09:59 +1100 Subject: [PATCH 06/18] Correct error check when encoding AVIF images (#9442) Co-authored-by: Andrew Murray --- src/_avif.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_avif.c b/src/_avif.c index 3585297fe..5e8b9fe8e 100644 --- a/src/_avif.c +++ b/src/_avif.c @@ -485,7 +485,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) { frame = image; } else { frame = avifImageCreateEmpty(); - if (image == NULL) { + if (frame == NULL) { PyErr_SetString(PyExc_ValueError, "Image creation failed"); return NULL; } From 4777a0b31820f184c01d550fa526400dc9b53eaf Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:21:48 +0300 Subject: [PATCH 07/18] Fix BMP RLE delta escape reading from wrong file position (#9443) Co-authored-by: Andrew Murray --- Tests/images/pal8rletrns.png | Bin 0 -> 4089 bytes Tests/test_file_bmp.py | 5 +++++ src/PIL/BmpImagePlugin.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 Tests/images/pal8rletrns.png diff --git a/Tests/images/pal8rletrns.png b/Tests/images/pal8rletrns.png new file mode 100644 index 0000000000000000000000000000000000000000..2362266ef0287fd543aaae2b3769cbdf49386ce5 GIT binary patch literal 4089 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 I2Cqo{A2mRU6951J literal 0 HcmV?d00001 diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 28e863459..2e0394b3b 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -221,6 +221,11 @@ def test_rle8_eof(file_name: str, length: int) -> None: im.load() +def test_rle_delta() -> None: + with Image.open("Tests/images/bmp/q/pal8rletrns.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/pal8rletrns.png") + + def test_unsupported_bmp_bitfields_layout() -> None: fp = io.BytesIO( o32(40) # header size diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index a12271370..5ee61b35b 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -369,7 +369,7 @@ class BmpRleDecoder(ImageFile.PyDecoder): bytes_read = self.fd.read(2) if len(bytes_read) < 2: break - right, up = self.fd.read(2) + right, up = bytes_read data += b"\x00" * (right + up * self.state.xsize) x = len(data) % self.state.xsize else: From 43c12af7301a77c461f23df3a1dcda25857bea5c Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:23:38 +0300 Subject: [PATCH 08/18] Fix `self.decode` typo (#9445) Co-authored-by: Andrew Murray --- src/PIL/ImageFile.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 341435437..50e0075a2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -579,10 +579,7 @@ class Parser: pass # not enough data else: flag = hasattr(im, "load_seek") or hasattr(im, "load_read") - if flag or len(im.tile) != 1: - # custom load code, or multiple tiles - self.decode = None - else: + if not flag and len(im.tile) == 1: # initialize decoder im.load_prepare() d, e, o, a = im.tile[0] From 81e0cf2bc4023a054f80a1b6de0e8634db5d19e5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:17:59 +1100 Subject: [PATCH 09/18] Add check-case-conflict hook (#9446) --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7eb69d164..19be90c16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,6 +38,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: + - id: check-case-conflict - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - id: check-merge-conflict From 2fe7c421486a1f10e1bae1d7abe08b873d5be098 Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:01:24 +0300 Subject: [PATCH 10/18] Only close file handle in ImagePalette.save() if it was opened internally (#9444) Co-authored-by: Andrew Murray --- Tests/test_imagepalette.py | 13 ++++++++++--- src/PIL/ImagePalette.py | 27 ++++++++++++++++----------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 10b89a2c0..526beb656 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,6 +1,6 @@ from __future__ import annotations -from io import BytesIO +import io from pathlib import Path import pytest @@ -23,6 +23,13 @@ def test_reload() -> None: assert_image_equal(im.convert("RGB"), original.convert("RGB")) +def test_save_fp() -> None: + palette = ImagePalette.ImagePalette() + with io.StringIO() as fp: + palette.save(fp) + assert not fp.closed + + def test_getcolor() -> None: palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 @@ -204,7 +211,7 @@ def test_2bit_palette(tmp_path: Path) -> None: def test_getpalette() -> None: - b = BytesIO(b"0 1\n1 2 3 4") + b = io.BytesIO(b"0 1\n1 2 3 4") p = PaletteFile.PaletteFile(b) palette, rawmode = p.getpalette() @@ -216,6 +223,6 @@ def test_invalid_palette() -> None: with pytest.raises(OSError): ImagePalette.load("Tests/images/hopper.jpg") - b = BytesIO(b"1" * 101) + b = io.BytesIO(b"1" * 101) with pytest.raises(SyntaxError, match="bad palette file"): PaletteFile.PaletteFile(b) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index eae7aea8f..99ad2771b 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -191,19 +191,24 @@ class ImagePalette: if self.rawmode: msg = "palette contains raw palette data" raise ValueError(msg) + open_fp = False if isinstance(fp, str): fp = open(fp, "w") - fp.write("# Palette\n") - fp.write(f"# Mode: {self.mode}\n") - 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("\n") - fp.close() + open_fp = True + try: + fp.write("# Palette\n") + fp.write(f"# Mode: {self.mode}\n") + 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("\n") + finally: + if open_fp: + fp.close() # -------------------------------------------------------------------- From e96c5a5a5321a7a5541811b41940b89bd1a59f9e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:17:12 +1100 Subject: [PATCH 11/18] Updated libpng to 1.6.55 (#9425) --- .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 a9b779e81..0057f62d6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -96,7 +96,7 @@ else FREETYPE_VERSION=2.14.1 fi HARFBUZZ_VERSION=12.3.2 -LIBPNG_VERSION=1.6.54 +LIBPNG_VERSION=1.6.55 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 bd9bd06b6..43c58d15f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ V = { "LCMS2": "2.18", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.4.1", - "LIBPNG": "1.6.54", + "LIBPNG": "1.6.55", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.4", "TIFF": "4.7.1", From 26c70950e9a5880dab8b510acf35f9faff5cbfba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Feb 2026 08:13:18 +1100 Subject: [PATCH 12/18] Use walrus operator --- src/PIL/MpoImagePlugin.py | 9 ++++----- src/PIL/PngImagePlugin.py | 12 ++++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 9360061ba..bee0a56f9 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -59,11 +59,10 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + b"MPF\0" + b" " * ifd_length ) - exif = im_frame.encoderinfo.get("exif") - if isinstance(exif, Image.Exif): - exif = exif.tobytes() - im_frame.encoderinfo["exif"] = exif - if exif: + if exif := im_frame.encoderinfo.get("exif"): + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_frame.encoderinfo["exif"] = exif mpf_offset += 4 + len(exif) JpegImagePlugin._save(im_frame, fp, filename) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 9826a4cd1..572762e6c 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1403,8 +1403,7 @@ def _save( chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] - icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: + if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")): # ICC profile # according to PNG spec, the iCCP chunk contains: # Profile name 1-79 bytes (character string) @@ -1419,8 +1418,7 @@ def _save( # Disallow sRGB chunks when an iCCP-chunk has been emitted. chunks.remove(b"sRGB") - info = im.encoderinfo.get("pnginfo") - if info: + if info := im.encoderinfo.get("pnginfo"): chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] for info_chunk in info.chunks: cid, data = info_chunk[:2] @@ -1472,8 +1470,7 @@ def _save( alpha_bytes = colors chunk(fp, b"tRNS", alpha[:alpha_bytes]) - dpi = im.encoderinfo.get("dpi") - if dpi: + if dpi := im.encoderinfo.get("dpi"): chunk( fp, b"pHYs", @@ -1490,8 +1487,7 @@ def _save( chunks.remove(cid) chunk(fp, cid, data) - exif = im.encoderinfo.get("exif") - if exif: + if exif := im.encoderinfo.get("exif"): if isinstance(exif, Image.Exif): exif = exif.tobytes(8) if exif.startswith(b"Exif\x00\x00"): From f273619682634fe6437d0cdb7858c18745ea31bc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:43:06 +0200 Subject: [PATCH 13/18] Test on macos-26-intel --- .github/workflows/test.yml | 2 +- .github/workflows/wheels.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 471cab90e..80bbfb45f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } # Intel - - { os: "macos-15-intel", python-version: "3.10" } + - { os: "macos-26-intel", python-version: "3.10" } exclude: - { os: "macos-latest", python-version: "3.10" } diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 20379c753..910c7202b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -53,19 +53,19 @@ jobs: include: - name: "macOS 10.10 x86_64" platform: macos - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64 build: "cp3{10,11}*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" platform: macos - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64 build: "cp3{12,13}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" platform: macos - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64 build: "{cp314,pp3}*" macosx_deployment_target: "10.15" @@ -104,7 +104,7 @@ jobs: cibw_arch: arm64_iphonesimulator - name: "iOS x86_64 simulator" platform: ios - os: macos-15-intel + os: macos-26-intel cibw_arch: x86_64_iphonesimulator steps: - uses: actions/checkout@v6 From 0c2dc2047edcd69a29445b7c91a75dfe0b85f3eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:25:35 +0000 Subject: [PATCH 14/18] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.14 → v0.15.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.14...v0.15.4) - [github.com/PyCQA/bandit: 1.9.3 → 1.9.4](https://github.com/PyCQA/bandit/compare/1.9.3...1.9.4) - [github.com/pre-commit/mirrors-clang-format: v21.1.8 → v22.1.0](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.8...v22.1.0) - [github.com/python-jsonschema/check-jsonschema: 0.36.1 → 0.37.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.36.1...0.37.0) - [github.com/tox-dev/pyproject-fmt: v2.12.1 → v2.16.2](https://github.com/tox-dev/pyproject-fmt/compare/v2.12.1...v2.16.2) - [github.com/abravalheri/validate-pyproject: v0.24.1 → v0.25](https://github.com/abravalheri/validate-pyproject/compare/v0.24.1...v0.25) --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19be90c16..53fd0a3ca 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.14.14 + rev: v0.15.4 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.9.3 + rev: 1.9.4 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.8 + rev: v22.1.0 hooks: - id: clang-format types: [c] @@ -52,7 +52,7 @@ repos: exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.36.1 + rev: 0.37.0 hooks: - id: check-github-workflows - id: check-readthedocs @@ -69,12 +69,12 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.12.1 + rev: v2.16.2 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.24.1 + rev: v0.25 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] From 7fc49a5cf413833d14c857d5829cd908d09ec7b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:27:42 +0000 Subject: [PATCH 15/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyproject.toml | 60 ++++++++--------------- src/Tk/tkImaging.c | 6 ++- src/encode.c | 5 +- src/libImaging/Arrow.c | 90 ++++++++++++++++++----------------- src/libImaging/Convert.c | 14 ++++-- src/libImaging/Draw.c | 11 +++-- src/libImaging/GetBBox.c | 10 ++-- src/libImaging/Jpeg2KEncode.c | 6 ++- src/libImaging/JpegDecode.c | 6 ++- src/libImaging/Matrix.c | 5 +- 10 files changed, 106 insertions(+), 107 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 91f4750e4..4b969dbc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,6 @@ optional-dependencies.test-arrow = [ "nanoarrow", "pyarrow", ] - optional-dependencies.tests = [ "check-manifest", "coverage>=7.4.2", @@ -77,16 +76,15 @@ optional-dependencies.tests = [ "pytest-xdist", "trove-classifiers>=2024.10.12", ] - optional-dependencies.xmp = [ "defusedxml", ] +urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Changelog = "https://github.com/python-pillow/Pillow/releases" urls.Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" urls.Homepage = "https://python-pillow.github.io" urls.Mastodon = "https://fosstodon.org/@pillow" -urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Source = "https://github.com/python-pillow/Pillow" [tool.setuptools] @@ -95,70 +93,50 @@ packages = [ ] include-package-data = true package-dir = { "" = "src" } - -[tool.setuptools.dynamic] -version = { attr = "PIL.__version__" } +dynamic.version = { attr = "PIL.__version__" } [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 - config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" - test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" test-requires = [ "numpy", ] -xbuild-tools = [ ] - -[tool.cibuildwheel.ios] +xbuild-tools = [] # Disable platform guessing on iOS, and disable raqm (since there won't be a # vendor version, and we can't distribute it due to licensing) -config-settings = "raqm=disable imagequant=disable platform-guessing=disable" - +ios.config-settings = "raqm=disable imagequant=disable platform-guessing=disable" # iOS needs to be given a specific pytest invocation and list of test sources. -test-sources = [ +ios.test-sources = [ "checks", "Tests", "selftest.py", ] -test-command = [ +ios.test-command = [ "python -m selftest", "python -m pytest -vv -x -W always checks/check_wheel.py Tests", ] - # There's no numpy wheel for iOS (yet...) -test-requires = [ ] - -[tool.cibuildwheel.macos] +ios.test-requires = [] # Disable platform guessing on macOS to avoid picking up Homebrew etc. -config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" - -[tool.cibuildwheel.macos.environment] +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" # Isolate macOS build environment from Homebrew etc. -PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" - -[[tool.cibuildwheel.overrides]] -# iOS environment is isolated by cibuildwheel, but needs the dependencies -select = "*_iphoneos" -environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" - -[[tool.cibuildwheel.overrides]] -# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies -select = "*_iphonesimulator" -environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" - -[[tool.cibuildwheel.overrides]] -select = "*-win32" -test-requires = [ ] +macos.environment.PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +overrides = [ + # iOS environment is isolated by cibuildwheel, but needs the dependencies + { select = "*_iphoneos", environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" }, + # iOS simulator environment is isolated by cibuildwheel, but needs the dependencies + { select = "*_iphonesimulator", environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" }, + { select = "*-win32", test-requires = [] }, +] [tool.black] exclude = "wheels/multibuild" [tool.ruff] exclude = [ "wheels/multibuild" ] - fix = true lint.select = [ "C4", # flake8-comprehensions @@ -207,9 +185,9 @@ lint.isort.required-imports = [ [tool.pyproject-fmt] max_supported_python = "3.14" -[tool.pytest.ini_options] -addopts = "-ra --color=auto" -testpaths = [ +[tool.pytest] +ini_options.addopts = "-ra --color=auto" +ini_options.testpaths = [ "Tests", ] diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 834634bd7..3e35f885f 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -124,8 +124,10 @@ PyImagingPhotoPut( if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) { block.pixelSize = 1; block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0; - } else if (im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || - im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa) { + } else if ( + im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa + ) { block.pixelSize = 4; block.offset[0] = 0; block.offset[1] = 1; diff --git a/src/encode.c b/src/encode.c index 06e4a0893..ea57615be 100644 --- a/src/encode.c +++ b/src/encode.c @@ -999,8 +999,9 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, PyBytes_AsString(value) ); - } else if (type == TIFF_DOUBLE || type == TIFF_SRATIONAL || - type == TIFF_RATIONAL) { + } else if ( + type == TIFF_DOUBLE || type == TIFF_SRATIONAL || type == TIFF_RATIONAL + ) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) ); diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index d2ed10f0a..de4d3568e 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -170,16 +170,17 @@ export_named_type(struct ArrowSchema *schema, char *format, const char *name) { strncpy(formatp, format, format_len); strncpy(namep, name, name_len); - *schema = (struct ArrowSchema){// Type description - .format = formatp, - .name = namep, - .metadata = NULL, - .flags = 0, - .n_children = 0, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &ReleaseExportedSchema + *schema = (struct ArrowSchema){ + // Type description + .format = formatp, + .name = namep, + .metadata = NULL, + .flags = 0, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &ReleaseExportedSchema }; return 0; } @@ -287,17 +288,18 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) { im->refcount++; MUTEX_UNLOCK(&im->mutex); // Initialize primitive fields - *array = (struct ArrowArray){// Data description - .length = length, - .offset = 0, - .null_count = 0, - .n_buffers = 2, - .n_children = 0, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &release_const_array, - .private_data = im + *array = (struct ArrowArray){ + // Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im }; // Allocate list of buffers @@ -332,17 +334,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { // Initialize primitive fields // Fixed length arrays are 1 buffer of validity, and the length in pixels. // Data is in a child array. - *array = (struct ArrowArray){// Data description - .length = length, - .offset = 0, - .null_count = 0, - .n_buffers = 1, - .n_children = 1, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &release_const_array, - .private_data = im + *array = (struct ArrowArray){ + // Data description + .length = length, + .offset = 0, + .null_count = 0, + .n_buffers = 1, + .n_children = 1, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im }; // Allocate list of buffers @@ -367,17 +370,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { MUTEX_LOCK(&im->mutex); im->refcount++; MUTEX_UNLOCK(&im->mutex); - *array->children[0] = (struct ArrowArray){// Data description - .length = length * 4, - .offset = 0, - .null_count = 0, - .n_buffers = 2, - .n_children = 0, - .children = NULL, - .dictionary = NULL, - // Bookkeeping - .release = &release_const_array, - .private_data = im + *array->children[0] = (struct ArrowArray){ + // Data description + .length = length * 4, + .offset = 0, + .null_count = 0, + .n_buffers = 2, + .n_children = 0, + .children = NULL, + .dictionary = NULL, + // Bookkeeping + .release = &release_const_array, + .private_data = im }; array->children[0]->buffers = diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 330e5325c..002497c32 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1695,16 +1695,20 @@ ImagingConvertTransparent(Imaging imIn, const ModeID mode, int r, int g, int b) if (mode == IMAGING_MODE_RGBa) { premultiplied = 1; } - } else if (imIn->mode == IMAGING_MODE_RGB && - (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)) { + } else if ( + imIn->mode == IMAGING_MODE_RGB && + (mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La) + ) { convert = rgb2la; source_transparency = 1; if (mode == IMAGING_MODE_La) { premultiplied = 1; } - } else if ((imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I || - imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) && - (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA)) { + } else if ( + (imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I || + imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) && + (mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA) + ) { if (imIn->mode == IMAGING_MODE_1) { convert = bit2rgb; } else if (imIn->mode == IMAGING_MODE_I) { diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index d28980432..0d28069f0 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -537,8 +537,9 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if ((ymin == current->ymin || ymin == current->ymax) && - current->dx != 0) { + } else if ( + (ymin == current->ymin || ymin == current->ymax) && current->dx != 0 + ) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; @@ -570,8 +571,10 @@ polygon_generic( adjacent_line_x, adjacent_line_x_other_edge )) + 1; - } else if (xx[j - 1] < adjacent_line_x - 1 && - xx[j - 1] < adjacent_line_x_other_edge - 1) { + } else if ( + xx[j - 1] < adjacent_line_x - 1 && + xx[j - 1] < adjacent_line_x_other_edge - 1 + ) { xx[j - 1] = roundf(fmin( adjacent_line_x, adjacent_line_x_other_edge diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index d336121d5..7a57f6894 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -89,10 +89,12 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if (alpha_only && - (im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || - im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || - im->mode == IMAGING_MODE_PA)) { + } else if ( + alpha_only && + (im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA || + im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA || + im->mode == IMAGING_MODE_PA) + ) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index fdfbde2d7..3012783a2 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -330,8 +330,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { components = 4; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgba; -#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ - (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2) +#if ( \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2 \ +) } else if (im->mode == IMAGING_MODE_CMYK) { components = 4; color_space = OPJ_CLRSPC_CMYK; diff --git a/src/libImaging/JpegDecode.c b/src/libImaging/JpegDecode.c index ae3274456..05cb37554 100644 --- a/src/libImaging/JpegDecode.c +++ b/src/libImaging/JpegDecode.c @@ -206,8 +206,10 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by context->cinfo.out_color_space = JCS_EXT_RGBX; } #endif - else if (context->rawmode == IMAGING_RAWMODE_CMYK || - context->rawmode == IMAGING_RAWMODE_CMYK_I) { + else if ( + context->rawmode == IMAGING_RAWMODE_CMYK || + context->rawmode == IMAGING_RAWMODE_CMYK_I + ) { context->cinfo.out_color_space = JCS_CMYK; } else if (context->rawmode == IMAGING_RAWMODE_YCbCr) { context->cinfo.out_color_space = JCS_YCbCr; diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index d28e04edf..acd59ba7f 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -46,8 +46,9 @@ ImagingConvertMatrix(Imaging im, const ModeID mode, float m[]) { } } ImagingSectionLeave(&cookie); - } else if (mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || - mode == IMAGING_MODE_RGB) { + } else if ( + mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || mode == IMAGING_MODE_RGB + ) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); if (!imOut) { return NULL; From 0fae74731d4b15f8496c263879d6de3895f9e7e1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:36:24 +1100 Subject: [PATCH 16/18] Update actions/download-artifact action to v8 (#9451) --- .github/workflows/wheels.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 20379c753..a697ff477 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -250,7 +250,7 @@ jobs: runs-on: ubuntu-latest name: Count dists steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: dist-* path: dist @@ -269,7 +269,7 @@ jobs: runs-on: ubuntu-latest name: Upload wheels to scientific-python-nightly-wheels steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: dist-!(sdist)* path: dist @@ -291,7 +291,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: dist-* path: dist From a8cf13010b7c76203b26dcd5c7534363a351b5ed Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:02:49 +1100 Subject: [PATCH 17/18] Use native configuration Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4b969dbc4..6d9910ca1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,8 +186,8 @@ lint.isort.required-imports = [ max_supported_python = "3.14" [tool.pytest] -ini_options.addopts = "-ra --color=auto" -ini_options.testpaths = [ +addopts = [ "-ra", "--color=auto" ] +testpaths = [ "Tests", ] From 55b0cbc27364b8b0ea01d7532ab509bdc6e65d32 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:01:13 +0200 Subject: [PATCH 18/18] Update CI targets docs --- docs/installation/platform-support.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 7a8707b9a..74c63fb06 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -39,15 +39,15 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 15 Sequoia | 3.10 | x86-64 | -| +----------------------------+---------------------+ -| | 3.11, 3.12, 3.13, 3.14, | arm64 | -| | PyPy3 | | +| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 | +| | 3.15, PyPy3 | | ++----------------------------------+----------------------------+---------------------+ +| macOS 26 Tahoe | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 | -| | 3.14, PyPy3 | | +| | 3.14, 3.15, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 | arm64v8, ppc64le, | | | | s390x | @@ -55,7 +55,7 @@ These platforms are built and tested for every change. | Windows Server 2022 | 3.10 | x86 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | -| | PyPy3 | | +| | 3.15, PyPy3 | | | +----------------------------+---------------------+ | | 3.13 (MinGW) | x86-64 | +----------------------------------+----------------------------+---------------------+