From 74ab5ac4cda564714545eee52ab789d4bddf1516 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Sun, 11 May 2025 23:46:21 +0200 Subject: [PATCH 01/34] Fix memory leak in arrow export using array structure --- src/libImaging/Arrow.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 33ff2ce77..36f4554fc 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -127,9 +127,7 @@ static void release_const_array(struct ArrowArray *array) { Imaging im = (Imaging)array->private_data; - if (array->n_children == 0) { - ImagingDelete(im); - } + ImagingDelete(im); // Free the buffers and the buffers array if (array->buffers) { From 4984c45da2f6b854cb49dc81fc56372f335d43a0 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:27:38 +0200 Subject: [PATCH 02/34] valgrind memory leak check --- Makefile | 6 ++++++ Tests/oss-fuzz/python.supp | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Makefile b/Makefile index 53164b08a..fd124d124 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,12 @@ valgrind: --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output +.PHONY: valgrind-leak +valgrind-leak: + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ + .PHONY: readme readme: python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 36385d672..1ea2a8eb5 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -14,3 +14,23 @@ fun:_TIFFReadEncodedTileAndAllocBuffer ... } + +{ + + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:_PyMem_RawMalloc + fun:PyObject_Malloc + ... +} + +{ + + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:_PyMem_RawRealloc + fun:PyMem_Realloc + ... +} From fdfba982c8d514240435f3ecef540939fb97f120 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:28:09 +0200 Subject: [PATCH 03/34] fix memory leak in arrow schema --- src/libImaging/Arrow.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 36f4554fc..7d3306123 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -37,6 +37,10 @@ ReleaseExportedSchema(struct ArrowSchema *array) { child->release = NULL; } // UNDONE -- should I be releasing the children? + free(array->children[i]); + } + if (array->children) { + free(array->children); } // Release dictionary @@ -117,6 +121,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) { retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); if (retval != 0) { free(schema->children[0]); + free(schema->children); schema->release(schema); return retval; } From 84b88a9fbc9c4cfd2bfb7d7a8d18225ee43efedb Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:58:12 +0200 Subject: [PATCH 04/34] Suppress all python level leaks for now --- Tests/oss-fuzz/python.supp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp index 1ea2a8eb5..4803497ad 100644 --- a/Tests/oss-fuzz/python.supp +++ b/Tests/oss-fuzz/python.supp @@ -18,7 +18,7 @@ { Memcheck:Leak - match-leak-kinds: possible + match-leak-kinds: all fun:malloc fun:_PyMem_RawMalloc fun:PyObject_Malloc @@ -28,7 +28,7 @@ { Memcheck:Leak - match-leak-kinds: possible + match-leak-kinds: all fun:malloc fun:_PyMem_RawRealloc fun:PyMem_Realloc From eaab43540344e26889116262651001f4e42b1630 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 10:58:37 +0200 Subject: [PATCH 05/34] Fix leak in webp_encode * Free the output buffer on webp encode error --- src/_webp.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 3aa4c408b..a7809c40e 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -641,6 +641,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ImagingSectionLeave(&cookie); WebPPictureFree(&pic); + + output = writer.mem; + ret_size = writer.size; + if (!ok) { int error_code = (&pic)->error_code; char message[50] = ""; @@ -652,10 +656,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); + free(output); return NULL; } - output = writer.mem; - ret_size = writer.size; { /* I want to truncate the *_size items that get passed into WebP From a9bcd7db884d89bbfe1966c0611f7f3dda1f8f08 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 19:50:55 +0200 Subject: [PATCH 06/34] Fix leak of destination image in ImagingUnsharpMask when an error occurs --- src/_imaging.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_imaging.c b/src/_imaging.c index 72f122143..79e0a2b23 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2226,6 +2226,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) { } if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { + ImagingDelete(imOut); return NULL; } From e2e40c54568236d2504921eb0b335cdab734a7d5 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 22:33:27 +0200 Subject: [PATCH 07/34] Fix memory leak in TiffEncode * If setimage errors out, the tiff client state was not freed. --- src/encode.c | 2 ++ src/libImaging/TiffDecode.c | 42 ++++++++++++++++++------------------- src/libImaging/TiffDecode.h | 2 ++ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/encode.c b/src/encode.c index 7c365a74f..e56494036 100644 --- a/src/encode.c +++ b/src/encode.c @@ -703,6 +703,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { return NULL; } + encoder->cleanup = ImagingLibTiffEncodeCleanup; + num_core_tags = sizeof(core_tags) / sizeof(int); for (pos = 0; pos < tags_size; pos++) { item = PyList_GetItemRef(tags, pos); diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 9a2db95b4..173eca160 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -929,6 +929,27 @@ ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) { return status; } +int +ImagingLibTiffEncodeCleanup(ImagingCodecState state) { + TIFFSTATE *clientstate = (TIFFSTATE *)state->context; + TIFF *tiff = clientstate->tiff; + + if (!tiff) { + return 0; + } + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup + if (clientstate->fp) { + // Python will manage the closing of the file rather than libtiff + // So only call TIFFCleanup + TIFFCleanup(tiff); + } else { + // When tif_closeproc refers to our custom _tiffCloseProc though, + // that is fine, as it does not close the file + TIFFClose(tiff); + } + return 0; +} + int ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) { /* One shot encoder. Encode everything to the tiff in the clientstate. @@ -1010,16 +1031,6 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; - // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup - if (clientstate->fp) { - // Python will manage the closing of the file rather than libtiff - // So only call TIFFCleanup - TIFFCleanup(tiff); - } else { - // When tif_closeproc refers to our custom _tiffCloseProc though, - // that is fine, as it does not close the file - TIFFClose(tiff); - } if (!clientstate->fp) { free(clientstate->data); } @@ -1036,22 +1047,11 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt TRACE(("Error flushing the tiff")); // likely reason is memory. state->errcode = IMAGING_CODEC_MEMORY; - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } if (!clientstate->fp) { free(clientstate->data); } return -1; } - TRACE(("Closing \n")); - if (clientstate->fp) { - TIFFCleanup(tiff); - } else { - TIFFClose(tiff); - } // reset the clientstate metadata to use it to read out the buffer. clientstate->loc = 0; clientstate->size = clientstate->eof; // redundant? diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 22361210d..77808b543 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -40,6 +40,8 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int +ImagingLibTiffEncodeCleanup(ImagingCodecState state); +extern int ImagingLibTiffMergeFieldInfo( ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length ); From f792e0b1ef4f25e0df33e8e971056142f9f5248d Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 22:48:36 +0200 Subject: [PATCH 08/34] Fix memory leak * Return after setting the error for advanced features without libraqm. Not returning here leads to an alloc that's never freed. --- src/_imagingft.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index 62dab73e5..ca8e556f0 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -425,6 +425,7 @@ text_layout_fallback( "setting text direction, language or font features is not supported " "without libraqm" ); + return 0; } if (PyUnicode_Check(string)) { From 789631c60c3760beeb623cd1728a737502fd9ca3 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 23:31:09 +0200 Subject: [PATCH 09/34] Fix memory leak when JpegEncode returns an error. --- src/libImaging/JpegEncode.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 3c11eac22..79a38e12f 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -131,6 +131,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { break; default: state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } @@ -161,6 +162,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Would subsample the green and blue channels, which doesn't make sense */ state->errcode = IMAGING_CODEC_CONFIG; + jpeg_destroy_compress(&context->cinfo); return -1; } jpeg_set_colorspace(&context->cinfo, JCS_RGB); From 7aa6a61d430c585cd10c91c5a73ce13f9f851b9e Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Tue, 13 May 2025 23:50:52 +0200 Subject: [PATCH 10/34] Wrap Makefile --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fd124d124..15f03ba45 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,8 @@ valgrind: .PHONY: valgrind-leak valgrind-leak: - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ + --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ From fb126af7a6a12e0870e56187257f75f35fe8558b Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 21:10:48 +0200 Subject: [PATCH 11/34] Adding pytest-valgrind install --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 15f03ba45..bdddecda5 100644 --- a/Makefile +++ b/Makefile @@ -101,6 +101,7 @@ valgrind: .PHONY: valgrind-leak valgrind-leak: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ From d5449d576013566100d8a0d41bbc1a756df86da5 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 21:11:31 +0200 Subject: [PATCH 12/34] Guess so. --- src/libImaging/Arrow.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c index 7d3306123..0b8c89a07 100644 --- a/src/libImaging/Arrow.c +++ b/src/libImaging/Arrow.c @@ -36,7 +36,6 @@ ReleaseExportedSchema(struct ArrowSchema *array) { child->release(child); child->release = NULL; } - // UNDONE -- should I be releasing the children? free(array->children[i]); } if (array->children) { From 218f055865a4f0abd05ac221c48cf86127907ca9 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 21:59:02 +0200 Subject: [PATCH 13/34] Add github workflow/test-script --- .github/workflows/test-valgrind-memory.yml | 60 ++++++++++++++++++++++ depends/docker-test-valgrind-memory.sh | 11 ++++ 2 files changed, 71 insertions(+) create mode 100644 .github/workflows/test-valgrind-memory.yml create mode 100644 depends/docker-test-valgrind-memory.sh diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml new file mode 100644 index 000000000..e6a5f6e77 --- /dev/null +++ b/.github/workflows/test-valgrind-memory.yml @@ -0,0 +1,60 @@ +name: Test Valgrind Memory Leaks + +# like the Docker tests, but running valgrind only on *.c/*.h changes. + +# this is very expensive. Only run on the pull request. +on: + # push: + # branches: + # - "**" + # paths: + # - ".github/workflows/test-valgrind.yml" + # - "**.c" + # - "**.h" + pull_request: + paths: + - ".github/workflows/test-valgrind.yml" + - "**.c" + - "**.h" + - "depends/docker-test-valgrind-memory.sh" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + docker: [ + ubuntu-22.04-jammy-amd64-valgrind, + ] + dockerTag: [main] + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Docker pull + run: | + docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Build and Run Valgrind + run: | + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE + docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh + sudo chown -R runner $GITHUB_WORKSPACE diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh new file mode 100644 index 000000000..4fd6652d8 --- /dev/null +++ b/depends/docker-test-valgrind-memory.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +## Run this as the test script in the docker valgrind image. +## Note -- can be included directly into the docker image, +## but requires the currnet python.supp. + +source /vpy3/bin/activate +cd /Pillow +make clean +make install +make valgrind-memory From a6b8b3af7709081d8c53818e68b8bc15e9a48f34 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 22:04:14 +0200 Subject: [PATCH 14/34] executable --- depends/docker-test-valgrind-memory.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 depends/docker-test-valgrind-memory.sh diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh old mode 100644 new mode 100755 From 2d506f6f5a478b4a798b3ce71f31b5e5f6f6b60f Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Thu, 15 May 2025 22:06:35 +0200 Subject: [PATCH 15/34] correct target --- depends/docker-test-valgrind-memory.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index 4fd6652d8..29fc6f230 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -8,4 +8,4 @@ source /vpy3/bin/activate cd /Pillow make clean make install -make valgrind-memory +make valgrind-leak From f1957b49b2d01a9d063aed4000f985b220e30fa0 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 16 May 2025 12:08:45 +0200 Subject: [PATCH 16/34] Xfail timouts in Valgrind tests * ensure that the env variable is set in the makefile --- Makefile | 4 ++-- Tests/test_file_jpeg.py | 5 +++++ Tests/test_imagefontpil.py | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bdddecda5..82c2c085f 100644 --- a/Makefile +++ b/Makefile @@ -95,14 +95,14 @@ test: .PHONY: valgrind valgrind: python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output .PHONY: valgrind-leak valgrind-leak: python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ + PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 79f0ec1a8..fb9f26fc7 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1034,6 +1034,11 @@ class TestFileJpeg: im.save(f, xmp=b"1" * 65505) @pytest.mark.timeout(timeout=1) + @pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower", + raises=TimeoutError + ) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 695aecbde..adce4a75c 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -2,6 +2,7 @@ from __future__ import annotations import struct from io import BytesIO +import os import pytest @@ -73,6 +74,11 @@ def test_decompression_bomb() -> None: @pytest.mark.timeout(4) +@pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower", + raises=TimeoutError +) def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 From ff50e30d3e9f1425ca6af95ac044d365c63719d1 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 16 May 2025 12:47:22 +0200 Subject: [PATCH 17/34] Fix memory leak in text_layout_raqm on 0 length string --- src/_imagingft.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index ca8e556f0..0d70544a5 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -275,6 +275,7 @@ text_layout_raqm( if (!text || !size) { /* return 0 and clean up, no glyphs==no size, and raqm fails with empty strings */ + PyMem_Free(text); goto failed; } set_text = raqm_set_text(rq, text, size); From 20b49a332bd0f0f39660fbb3587cfc4b6d539f0c Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Sat, 17 May 2025 10:45:43 +0200 Subject: [PATCH 18/34] Remove timeout as the specific reason, pytest-timeout doesn't raise a timeout error. --- Tests/test_file_jpeg.py | 3 +-- Tests/test_imagefontpil.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fb9f26fc7..d923020c8 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1036,8 +1036,7 @@ class TestFileJpeg: @pytest.mark.timeout(timeout=1) @pytest.mark.xfail( "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower", - raises=TimeoutError + reason="Valgrind is slower" ) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index adce4a75c..bd9bafb55 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -76,8 +76,7 @@ def test_decompression_bomb() -> None: @pytest.mark.timeout(4) @pytest.mark.xfail( "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower", - raises=TimeoutError + reason="Valgrind is slower" ) def test_oom() -> None: glyph = struct.pack( From c35082b619899d5351ba249e8ea23a4412d0c728 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 08:47:59 +0000 Subject: [PATCH 19/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 3 +-- Tests/test_imagefontpil.py | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index d923020c8..7c33c7517 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1035,8 +1035,7 @@ class TestFileJpeg: @pytest.mark.timeout(timeout=1) @pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index bd9bafb55..e5b770745 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,8 +1,8 @@ from __future__ import annotations +import os import struct from io import BytesIO -import os import pytest @@ -74,10 +74,7 @@ def test_decompression_bomb() -> None: @pytest.mark.timeout(4) -@pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" -) +@pytest.mark.xfail("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 From 2603a249df9223133b74c671acfcdc6a51567843 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 23 May 2025 10:57:03 +0100 Subject: [PATCH 20/34] Update depends/docker-test-valgrind-memory.sh Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/docker-test-valgrind-memory.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index 29fc6f230..5f7805421 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -2,7 +2,7 @@ ## Run this as the test script in the docker valgrind image. ## Note -- can be included directly into the docker image, -## but requires the currnet python.supp. +## but requires the current python.supp. source /vpy3/bin/activate cd /Pillow From 60a1a20536fe18cfe936e90140ed56c3eb31bd81 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 23 May 2025 15:32:46 +0200 Subject: [PATCH 21/34] add timeouts to two more tests --- Tests/test_file_tiff.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 502d9df9a..050bfe578 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -990,6 +990,10 @@ class TestFileTiff: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") + @pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower" + ) def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) @@ -1002,6 +1006,10 @@ class TestFileTiff: ], ) @pytest.mark.timeout(2) + @pytest.mark.xfail( + "PILLOW_VALGRIND_TEST" in os.environ, + reason="Valgrind is slower" + ) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): From c63db77db3850c51df38af8f4a96f5c13f286b42 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 13:37:02 +0000 Subject: [PATCH 22/34] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_tiff.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 050bfe578..b6985b83b 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -991,8 +991,7 @@ class TestFileTiff: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") @pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: @@ -1007,8 +1006,7 @@ class TestFileTiff: ) @pytest.mark.timeout(2) @pytest.mark.xfail( - "PILLOW_VALGRIND_TEST" in os.environ, - reason="Valgrind is slower" + "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): From 4d0678ca33b65af2686fe93be5b77c2b28027959 Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 23 May 2025 16:35:57 +0200 Subject: [PATCH 23/34] Add parallel test target, using pytest-xdist --- Makefile | 8 +++++++- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5a8152454..a56fe8fec 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,13 @@ sdist: .PHONY: test test: python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq + python3 -m pytest -qq Tests/ + +.PHONY: test-p +test-p: + python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist + python3 -m pytest -qq -n auto Tests/ + .PHONY: valgrind valgrind: diff --git a/pyproject.toml b/pyproject.toml index a3ff9723b..683ab24ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ optional-dependencies.tests = [ "pytest", "pytest-cov", "pytest-timeout", + "pytest-xdist", "trove-classifiers>=2024.10.12", ] From 6a60b2e6dd0909f627d093cbc431891a79d2b987 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:27:11 +0100 Subject: [PATCH 24/34] Remove Tests/ path arg, this is already configured --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a56fe8fec..1f9d2ce13 100644 --- a/Makefile +++ b/Makefile @@ -95,12 +95,12 @@ sdist: .PHONY: test test: python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq Tests/ + python3 -m pytest -qq .PHONY: test-p test-p: python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist - python3 -m pytest -qq -n auto Tests/ + python3 -m pytest -qq -n auto .PHONY: valgrind From 98cf15e9e487cbc53b101498d43cc0cc141ee7e7 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:35:13 +0100 Subject: [PATCH 25/34] Update depends/docker-test-valgrind-memory.sh Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/docker-test-valgrind-memory.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index 5f7805421..f0d1d851d 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -1,7 +1,7 @@ #!/bin/bash -## Run this as the test script in the docker valgrind image. -## Note -- can be included directly into the docker image, +## Run this as the test script in the Docker valgrind image. +## Note -- can be included directly into the Docker image, ## but requires the current python.supp. source /vpy3/bin/activate From 399b6c1045ff2387e7db8206e72baec33f996030 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 30 May 2025 10:40:07 +0100 Subject: [PATCH 26/34] Update Makefile Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4f63cfe02..27d70dcb7 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,7 @@ valgrind-leak: PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ - python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output Tests/ + python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output .PHONY: readme readme: From 3944db288a5b54ea6171fd1334e517fbcc3c9136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=93=E9=BC=A0?= Date: Sat, 31 May 2025 09:10:45 +0800 Subject: [PATCH 27/34] Update MinGW package names (#8987) --- docs/installation/building-from-source.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index c72568b20..8988a92ce 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -194,9 +194,9 @@ Many of Pillow's features require external libraries: pacman -S \ mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools + mingw-w64-x86_64-python \ + mingw-w64-x86_64-python-pip \ + mingw-w64-x86_64-python-setuptools Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: From bc4138f1692d718ec9fe7b3b7449dc20d0e2d85e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 May 2025 11:48:49 +1000 Subject: [PATCH 28/34] ubuntu-latest now uses Ubuntu 24.04 --- docs/installation/platform-support.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 93486d034..1071380fd 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -42,11 +42,13 @@ These platforms are built and tested for every change. | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | | | PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, PyPy3 | | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, | -| | | ppc64le, s390x | +| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, 3.13, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.12 | arm64v8, ppc64le, | +| | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2019 | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ From 9327e425ba77523ec9d98eb9558806ecf29b9365 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 May 2025 12:02:16 +1000 Subject: [PATCH 29/34] Stop testing deprecated Windows Server 2019 --- .github/workflows/test-windows.yml | 5 ++--- docs/installation/platform-support.rst | 6 +++--- winbuild/README.md | 3 +-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index bfa4c7cd3..6b76351b0 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -31,16 +31,15 @@ env: jobs: build: - runs-on: ${{ matrix.os }} + runs-on: windows-latest strategy: fail-fast: false matrix: python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] architecture: ["x64"] - os: ["windows-latest"] include: # Test the oldest Python on 32-bit - - { python-version: "3.9", architecture: "x86", os: "windows-2019" } + - { python-version: "3.9", architecture: "x86" } timeout-minutes: 45 diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 93486d034..f262d861c 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -48,9 +48,9 @@ These platforms are built and tested for every change. | Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, | | | | ppc64le, s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2019 | 3.9 | x86 | -+----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| Windows Server 2022 | 3.9 | x86 | +| +----------------------------+---------------------+ +| | 3.10, 3.11, 3.12, 3.13, | x86-64 | | | PyPy3 | | | +----------------------------+---------------------+ | | 3.12 (MinGW) | x86-64 | diff --git a/winbuild/README.md b/winbuild/README.md index c474f12ce..0d3ec8d8a 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -11,8 +11,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires CMake 3.15 or newer (available as Visual Studio component). -* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server - 2019 with Visual Studio 2019 Enterprise (GitHub Actions). +* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). Here's an example script to build on Windows: From 892fd2c2affa4980121a059bc2b7875834571804 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 1 Jun 2025 15:41:48 +1000 Subject: [PATCH 30/34] Removed unreachable code (#8918) --- src/PIL/MpegImagePlugin.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 5aa00d05b..47ebe9d62 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -33,11 +33,7 @@ class BitStream: def peek(self, bits: int) -> int: while self.bits < bits: - c = self.next() - if c < 0: - self.bits = 0 - continue - self.bitbuffer = (self.bitbuffer << 8) + c + self.bitbuffer = (self.bitbuffer << 8) + self.next() self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 From 95603e9717c81d3492933c3a8d094bfbb7e90340 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:14:11 +1000 Subject: [PATCH 31/34] Use ImageFile.MAXBLOCK in tobytes() (#8906) --- src/PIL/Image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index aaa3332ee..ed2f728aa 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -802,7 +802,9 @@ class Image: e = _getencoder(self.mode, encoder_name, encoder_args) e.setimage(self.im) - bufsize = max(65536, self.size[0] * 4) # see RawEncode.c + from . import ImageFile + + bufsize = max(ImageFile.MAXBLOCK, self.size[0] * 4) # see RawEncode.c output = [] while True: From 070e1eba626736a5cfa4a90a8a97dfbbf6278b91 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:08:24 +1000 Subject: [PATCH 32/34] [pre-commit.ci] pre-commit autoupdate (#8993) --- .pre-commit-config.yaml | 8 ++++---- src/_imaging.c | 9 +++++---- src/display.c | 8 ++++---- src/libImaging/Fill.c | 5 +++-- src/libImaging/Filter.c | 6 ++++-- src/libImaging/Jpeg2KEncode.c | 8 ++++---- src/libImaging/Point.c | 5 +++-- src/libImaging/Resample.c | 6 ++++-- src/libImaging/Storage.c | 5 +++-- src/libImaging/TiffDecode.c | 3 ++- 10 files changed, 36 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e15e6f639..a1a054e00 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.11.8 + rev: v0.11.12 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.3 + rev: v20.1.5 hooks: - id: clang-format types: [c] @@ -58,7 +58,7 @@ repos: - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.6.0 + rev: v1.9.0 hooks: - id: zizmor @@ -68,7 +68,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.5.1 + rev: v2.6.0 hooks: - id: pyproject-fmt diff --git a/src/_imaging.c b/src/_imaging.c index 79e0a2b23..9213ba13d 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -308,9 +308,9 @@ _new_arrow(PyObject *self, PyObject *args) { } // ImagingBorrowArrow is responsible for retaining the array_capsule - ret = - PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) - ); + ret = PyImagingNew( + ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) + ); if (!ret) { return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); } @@ -1665,7 +1665,8 @@ _putdata(ImagingObject *self, PyObject *args) { int bigendian = 0; if (image->type == IMAGING_TYPE_SPECIAL) { // I;16* - if (strcmp(image->mode, "I;16B") == 0 + if ( + strcmp(image->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(image->mode, "I;16N") == 0 #endif diff --git a/src/display.c b/src/display.c index 11742a895..3215f6691 100644 --- a/src/display.c +++ b/src/display.c @@ -327,11 +327,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { // added in Windows 10 (1607) // loaded dynamically to avoid link errors user32 = LoadLibraryA("User32.dll"); - SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext - )GetProcAddress(user32, "SetThreadDpiAwarenessContext"); + SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext) + GetProcAddress(user32, "SetThreadDpiAwarenessContext"); if (SetThreadDpiAwarenessContext_function != NULL) { - GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext - )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); + GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext) + GetProcAddress(user32, "GetWindowDpiAwarenessContext"); if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) { dpiAwareness = GetWindowDpiAwarenessContext_function(wnd); } diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 8fb481e7e..28f427370 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -118,8 +118,9 @@ ImagingFillRadialGradient(const char *mode) { for (y = 0; y < 256; y++) { for (x = 0; x < 256; x++) { - d = (int - )sqrt((double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0); + d = (int)sqrt( + (double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0 + ); if (d >= 255) { d = 255; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 7b7b2e429..c46dd3cd1 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -155,7 +155,8 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif @@ -308,7 +309,8 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { } else { int bigendian = 0; if (im->type == IMAGING_TYPE_SPECIAL) { - if (strcmp(im->mode, "I;16B") == 0 + if ( + strcmp(im->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN || strcmp(im->mode, "I;16N") == 0 #endif diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 34d1a2294..61e095ad6 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -207,8 +207,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { if (params->cp_cinema == OPJ_CINEMA4K_24) { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_24_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_24_CS_LENGTH * 8)); params->POC[0].tile = 1; params->POC[0].resno0 = 0; @@ -243,8 +243,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) { params->max_comp_size = COMP_24_CS_MAX_LENGTH; } else { float max_rate = - ((float)(components * im->xsize * im->ysize * 8) / (CINEMA_48_CS_LENGTH * 8) - ); + ((float)(components * im->xsize * im->ysize * 8) / + (CINEMA_48_CS_LENGTH * 8)); for (n = 0; n < params->tcp_numlayers; ++n) { rate = 0; diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 6a4060b4b..b11ea62ed 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -197,8 +197,9 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { return imOut; mode_mismatch: - return (Imaging - )ImagingError_ValueError("point operation not supported for this mode"); + return (Imaging)ImagingError_ValueError( + "point operation not supported for this mode" + ); } Imaging diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index f5e386dc2..b114e0023 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -470,7 +470,8 @@ ImagingResampleHorizontal_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif @@ -509,7 +510,8 @@ ImagingResampleVertical_16bpc( double *k; int bigendian = 0; - if (strcmp(imIn->mode, "I;16N") == 0 + if ( + strcmp(imIn->mode, "I;16N") == 0 #ifdef WORDS_BIGENDIAN || strcmp(imIn->mode, "I;16B") == 0 #endif diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 6f0a1bfa3..11d6c06cc 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -602,8 +602,9 @@ ImagingBorrowArrow( } if (!borrowed_buffer) { - return (Imaging - )ImagingError_ValueError("Arrow Array, exactly 2 buffers required"); + return (Imaging)ImagingError_ValueError( + "Arrow Array, exactly 2 buffers required" + ); } for (y = i = 0; y < im->ysize; y++) { diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 173eca160..2e83fb847 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -557,7 +557,8 @@ _decodeStrip( (tdata_t)state->buffer, strip_size ) == -1) { - TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) + TRACE( + ("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0)) ); state->errcode = IMAGING_CODEC_BROKEN; return -1; From eb0256acc082e362b4172f3256a551a412ef4b09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Jun 2025 22:44:26 +1000 Subject: [PATCH 33/34] Fixed test --- Tests/test_deprecate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 82ff14181..88479ff0d 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -47,7 +47,6 @@ def test_unknown_version() -> None: ], ) def test_old_version(deprecated: str, plural: bool, expected: str) -> None: - expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) From f3b05d6fab2a8fb0db033fc507d0d6f19ed2330e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:07:21 +1000 Subject: [PATCH 34/34] Update dependency mypy to v1.16.0 (#8991) Co-authored-by: Andrew Murray --- .ci/requirements-mypy.txt | 2 +- Tests/test_file_jpeg.py | 10 ++++++---- Tests/test_image.py | 1 + src/PIL/GifImagePlugin.py | 9 ++++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 86ac2e0b2..a9c18ae2b 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.15.0 +mypy==1.16.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index b9eec591d..2827937cf 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -145,14 +145,16 @@ class TestFileJpeg: assert k > 0.9 # roundtrip, and check again im = self.roundtrip(im) - c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + cmyk = im.getpixel((0, 0)) + assert isinstance(cmyk, tuple) + c, m, y, k = (x / 255.0 for x in cmyk) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) + cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) + assert isinstance(cmyk, tuple) + k = cmyk[3] / 255.0 assert k > 0.9 def test_rgb(self) -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 14a067127..4cc841603 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -671,6 +671,7 @@ class TestImage: im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) assert im.palette is not None + assert im_remapped.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4392c4cb9..c98e02f69 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, Any, Literal, NamedTuple, Union +from typing import IO, Any, Literal, NamedTuple, Union, cast from . import ( Image, @@ -350,12 +350,15 @@ class GifImageFile(ImageFile.ImageFile): if self._frame_palette: if color * 3 + 3 > len(self._frame_palette.palette): color = 0 - return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + return cast( + tuple[int, int, int], + tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), + ) else: return (color, color, color) self.dispose = None - self.dispose_extent = frame_dispose_extent + self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent if self.dispose_extent and self.disposal_method >= 2: try: if self.disposal_method == 2: