From e10a58ccce18c3493e30dfe7222076964311a8d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Oct 2023 21:46:38 +1100 Subject: [PATCH 001/186] Concatenate multiple EXIF markers --- Tests/images/multiple_exif.jpg | Bin 0 -> 364 bytes Tests/test_file_jpeg.py | 4 ++++ src/PIL/JpegImagePlugin.py | 10 ++++++---- 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 Tests/images/multiple_exif.jpg diff --git a/Tests/images/multiple_exif.jpg b/Tests/images/multiple_exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..32e0aa301a94dfa13dc47faeb3071c25a5d8b573 GIT binary patch literal 364 zcmex=v-)3-T z;9z58XJh4HXJ_Z+gTWM0TY@5u?V53ptdXHXalWy7)oGIH{I3zSIJR&kGIVCkMJtH%#xTLhKyrQzI zxuvzOy`!^h(&Q;qr%j(RbJn88OO`HMzGCI7O`ErD-L`$l&RvHNA31vL_=%IJE?vHI v_1g6tH*Y Date: Fri, 24 Nov 2023 15:19:19 +1100 Subject: [PATCH 002/186] Do not assign new fp attribute to image when exiting context manager --- Tests/test_image.py | 5 +++++ src/PIL/Image.py | 17 +++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index f0861bb4f..98a82cfaa 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1015,6 +1015,11 @@ class TestImage: except OSError as e: assert str(e) == "buffer overrun when reading image file" + def test_exit_fp(self): + with Image.new("L", (1, 1)) as im: + pass + assert not hasattr(im, "fp") + def test_close_graceful(self, caplog): with Image.open("Tests/images/hopper.jpg") as im: copy = im.copy() diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d435b5617..b6ac1f082 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -528,14 +528,15 @@ class Image: return self def __exit__(self, *args): - if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False): - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() - self.fp = None + if hasattr(self, "fp"): + if getattr(self, "_exclusive_fp", False): + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + self.fp = None def close(self): """ From cd9deddcd5e09c4a31621a21acb2f059296162b1 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 28 Nov 2023 00:50:42 +0100 Subject: [PATCH 003/186] add gcc problem matcher to test.yml --- .github/problem-matchers/gcc.json | 18 ++++++++++++++++++ .github/workflows/test.yml | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 .github/problem-matchers/gcc.json diff --git a/.github/problem-matchers/gcc.json b/.github/problem-matchers/gcc.json new file mode 100644 index 000000000..8e2866afe --- /dev/null +++ b/.github/problem-matchers/gcc.json @@ -0,0 +1,18 @@ +{ + "__comment": "Based on vscode-cpptools' Extension/package.json gcc rule", + "problemMatcher": [ + { + "owner": "gcc-problem-matcher", + "pattern": [ + { + "regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33dc561e5..9e7d4ed62 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,6 +86,10 @@ jobs: env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + - name: Register gcc problem matcher + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" + run: echo "::add-matcher::.github/problem-matchers/gcc.json" + - name: Build run: | .ci/build.sh From 5fb86c55ed52fecf61daa9c9e93108abc1831948 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Nov 2023 20:05:17 +1100 Subject: [PATCH 004/186] Moved code closing fp and _fp into common method --- src/PIL/Image.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b6ac1f082..7e0e14f26 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -527,15 +527,18 @@ class Image: def __enter__(self): return self + def _close_fp(self): + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + def __exit__(self, *args): if hasattr(self, "fp"): if getattr(self, "_exclusive_fp", False): - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() + self._close_fp() self.fp = None def close(self): @@ -552,12 +555,7 @@ class Image: """ if hasattr(self, "fp"): try: - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() + self._close_fp() self.fp = None except Exception as msg: logger.debug("Error closing: %s", msg) From f1fef09d4ae34a060296ef312e908a3f70413d2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Nov 2023 21:05:53 +1100 Subject: [PATCH 005/186] Support arbitrary masks for uncompressed RGB images --- Tests/images/bgr15.dds | Bin 0 -> 32896 bytes Tests/images/bgr15.png | Bin 0 -> 17923 bytes Tests/test_file_dds.py | 2 ++ src/PIL/DdsImagePlugin.py | 48 +++++++++++++++++++++++++++++++++----- 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 Tests/images/bgr15.dds create mode 100644 Tests/images/bgr15.png diff --git a/Tests/images/bgr15.dds b/Tests/images/bgr15.dds new file mode 100644 index 0000000000000000000000000000000000000000..ba3bbddcae4c59aeb3049fc46527a94b1fadda55 GIT binary patch literal 32896 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 (offset + 1) << (offset + 1) == mask: + offset += 1 + mask_offsets.append(offset) + mask_totals.append(mask >> offset) + + data = bytearray() + bytecount = bitcount // 8 + while len(data) < self.state.xsize * self.state.ysize * len(masks): + value = self.fd.read(bytecount) + int_value = sum(value[i] << i * 8 for i in range(bytecount)) + for i, mask in enumerate(masks): + masked_value = int_value & mask + # Remove the zero padding, and scale it to 8 bits + data += o8( + int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) + ) + self.set_as_raw(bytes(data)) + return -1, 0 + + def _save(im, fp, filename): if im.mode not in ("RGB", "RGBA", "L", "LA"): msg = f"cannot write mode {im.mode} as DDS" @@ -291,5 +326,6 @@ def _accept(prefix): Image.register_open(DdsImageFile.format, DdsImageFile, _accept) +Image.register_decoder("dds_rgb", DdsRgbDecoder) Image.register_save(DdsImageFile.format, _save) Image.register_extension(DdsImageFile.format, ".dds") From 30eb41475dacea619c4660555788ec73d32df2a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Dec 2023 22:44:51 +1100 Subject: [PATCH 006/186] Use f-string Co-authored-by: Aarni Koskela --- src/PIL/DdsImagePlugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 65d9deaa0..731c85a7e 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -154,9 +154,7 @@ class DdsImageFile(ImageFile.ImageFile): self._mode = "RGB" mask_count = 3 - masks = struct.unpack( - "<" + str(mask_count) + "I", header.read(mask_count * 4) - ) + masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4)) self.tile = [("dds_rgb", (0, 0) + self.size, 0, (bitcount, masks))] elif pfflags & DDPF_PALETTEINDEXED8: self._mode = "P" From 2e8dd3bdcaf3fa13884ee3ca035c32ba9c4f7fcd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Dec 2023 22:56:25 +1100 Subject: [PATCH 007/186] Use int.from_bytes() --- src/PIL/DdsImagePlugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 731c85a7e..17b1a6082 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -258,10 +258,9 @@ class DdsRgbDecoder(ImageFile.PyDecoder): data = bytearray() bytecount = bitcount // 8 while len(data) < self.state.xsize * self.state.ysize * len(masks): - value = self.fd.read(bytecount) - int_value = sum(value[i] << i * 8 for i in range(bytecount)) + value = int.from_bytes(self.fd.read(bytecount), "little") for i, mask in enumerate(masks): - masked_value = int_value & mask + masked_value = value & mask # Remove the zero padding, and scale it to 8 bits data += o8( int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) From 232094e065e8667f3d8d6c2132c44d9426c86ef6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 21:45:19 +0000 Subject: [PATCH 008/186] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/DdsImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 2309cb406..5a1627686 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -16,8 +16,8 @@ import sys from enum import IntEnum, IntFlag from . import Image, ImageFile, ImagePalette -from ._binary import o8 from ._binary import i32le as i32 +from ._binary import o8 from ._binary import o32le as o32 # Magic ("DDS ") From 1ecd19336f4bb1a70381ba29b5c7109134d61d55 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 12 Dec 2023 20:22:29 +0100 Subject: [PATCH 009/186] cross-complie fribidi for windows arm64 --- .github/workflows/wheels.yml | 7 +---- winbuild/build_prepare.py | 31 +++++++++++--------- winbuild/fribidi.cmake | 56 ++++++++++++++++++++++-------------- 3 files changed, 53 insertions(+), 41 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 76d42b470..5c73a5273 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -116,10 +116,7 @@ jobs: & python.exe -m pip install -r .ci/requirements-cibw.txt - # Cannot cross-compile FriBiDi (only used for tests) - $FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}") - if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" } - & python.exe winbuild\build_prepare.py -v @FLAGS + & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }} shell: pwsh - name: Build wheels @@ -163,14 +160,12 @@ jobs: path: ./wheelhouse/*.whl - name: Prepare to upload FriBiDi - if: "matrix.arch != 'ARM64'" run: | mkdir fribidi\${{ matrix.arch }} copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }} shell: cmd - name: Upload fribidi.dll - if: "matrix.arch != 'ARM64'" uses: actions/upload-artifact@v3 with: name: fribidi diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f7e145fb9..67e603d0d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -56,7 +56,9 @@ def cmd_nmake( ) -def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]: +def cmds_cmake( + target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." +) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -73,10 +75,11 @@ def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]: "-DCMAKE_CXX_FLAGS=-nologo", *params, '-G "{cmake_generator}"', - ".", + f'-B "{build_dir}"', + f"-S .", ] ), - f"{{cmake}} --build . --clean-first --parallel --target {target}", + f'{{cmake}} --build "{build_dir}" --clean-first --parallel --target {target}', ] @@ -367,7 +370,14 @@ DEPS = { "build": [ cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), - *cmds_cmake("fribidi"), + # generated tab.i files cannot be cross-compiled + " ^&^& ".join( + [ + "if {architecture}==ARM64 cmd /c call {vcvarsall} x86", + *cmds_cmake("fribidi-gen", "-DARCH=x86", build_dir="build_x86"), + ] + ), + *cmds_cmake("fribidi", "-DARCH={architecture}"), ], "bins": [r"*.dll"], }, @@ -381,10 +391,9 @@ def find_msvs(architecture: str) -> dict[str, str] | None: print("Program Files not found") return None + requires = ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"] if architecture == "ARM64": - tools = "Microsoft.VisualStudio.Component.VC.Tools.ARM64" - else: - tools = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" + requires += ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.ARM64"] try: vspath = ( @@ -395,8 +404,7 @@ def find_msvs(architecture: str) -> dict[str, str] | None: ), "-latest", "-prerelease", - "-requires", - tools, + *requires, "-property", "installationPath", "-products", @@ -707,11 +715,6 @@ if __name__ == "__main__": disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] - elif args.architecture == "ARM64" and platform.machine() != "ARM64": - import warnings - - warnings.warn("Cross-compiling FriBiDi is currently not supported, disabling") - disabled += ["fribidi"] prefs = { "architecture": args.architecture, diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake index 27b8d17a8..b16e0784c 100644 --- a/winbuild/fribidi.cmake +++ b/winbuild/fribidi.cmake @@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.12) project(fribidi) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) include_directories(lib) function(extract_regex_1 var text regex) @@ -27,12 +27,20 @@ function(fribidi_conf) set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new") set(SIZEOF_INT 4) set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC") - message("detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}") - configure_file(lib/fribidi-config.h.in lib/fribidi-config.h @ONLY) + message("Detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}") + configure_file(lib/fribidi-config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/lib/fribidi-config.h @ONLY) endfunction() fribidi_conf() +option(ARCH "Target architecture") +if(${ARCH} STREQUAL ARM64) + set(GEN FALSE) +else() + set(GEN TRUE) +endif() +message("Generate tab.i files: " ${GEN}) + function(prepend var prefix) set(out "") foreach(f ${ARGN}) @@ -56,18 +64,20 @@ macro(fribidi_definitions _TGT) endmacro() function(fribidi_gen _NAME _OUTNAME _PARAM) - set(_OUT lib/${_OUTNAME}) - prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) - add_executable(gen-${_NAME} - gen.tab/gen-${_NAME}.c - gen.tab/packtab.c) - fribidi_definitions(gen-${_NAME}) - target_compile_definitions(gen-${_NAME} - PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H) - add_custom_command( - COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT} - DEPENDS ${_DEP} - OUTPUT ${_OUT}) + set(_OUT ${CMAKE_CURRENT_SOURCE_DIR}/lib/${_OUTNAME}) + if(GEN) + prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) + add_executable(gen-${_NAME} + gen.tab/gen-${_NAME}.c + gen.tab/packtab.c) + fribidi_definitions(gen-${_NAME}) + target_compile_definitions(gen-${_NAME} + PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H) + add_custom_command( + COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT} + DEPENDS ${_DEP} + OUTPUT ${_OUT}) + endif(GEN) list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}") set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE) endfunction() @@ -78,8 +88,10 @@ fribidi_gen(unicode-version fribidi-unicode-version.h "" macro(fribidi_tab _NAME) fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN}) - target_sources(gen-${_NAME}-tab - PRIVATE lib/fribidi-unicode-version.h) + if(GEN) + target_sources(gen-${_NAME}-tab + PRIVATE lib/fribidi-unicode-version.h) + endif(GEN) endmacro() fribidi_tab(bidi-type unidata/UnicodeData.txt) @@ -89,14 +101,16 @@ fribidi_tab(mirroring unidata/BidiMirroring.txt) fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt) fribidi_tab(brackets-type unidata/BidiBrackets.txt) +add_custom_target(fribidi-gen DEPENDS ${FRIBIDI_SOURCES_GENERATED}) + file(GLOB FRIBIDI_SOURCES lib/*.c) file(GLOB FRIBIDI_HEADERS lib/*.h) add_library(fribidi SHARED - ${FRIBIDI_SOURCES} - ${FRIBIDI_HEADERS} - ${FRIBIDI_SOURCES_GENERATED}) + ${FRIBIDI_SOURCES} + ${FRIBIDI_HEADERS} + ${FRIBIDI_SOURCES_GENERATED}) fribidi_definitions(fribidi) target_compile_definitions(fribidi - PUBLIC "-DFRIBIDI_BUILD") + PUBLIC "-DFRIBIDI_BUILD") From 4b422db2439d1858cc71f38e706775daa419b692 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Tue, 24 Oct 2023 23:55:19 -0500 Subject: [PATCH 010/186] Add keep_rgb option to prevent RGB -> YCbCr conversion during JPEG write libjpeg automatically converts RGB to YCbCr by default. Add a keep_rgb option to disable libjpeg's automatic conversion of RGB images during write. --- Tests/test_file_jpeg.py | 14 ++++++++++++++ docs/handbook/image-file-formats.rst | 7 +++++++ src/PIL/JpegImagePlugin.py | 1 + src/encode.c | 5 ++++- src/libImaging/Jpeg.h | 3 +++ src/libImaging/JpegEncode.c | 14 ++++++++++++++ 6 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ef070b6c5..3b11c1123 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -141,6 +141,16 @@ class TestFileJpeg: ) assert k > 0.9 + def test_rgb(self): + def getchannels(im): + return tuple(v[0] for v in im.layer) + + im = self.roundtrip(hopper()) + assert getchannels(im) == (1, 2, 3) + im = self.roundtrip(hopper(), keep_rgb=True) + assert getchannels(im) == (ord("R"), ord("G"), ord("B")) + assert_image_similar(hopper(), im, 12) + @pytest.mark.parametrize( "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], @@ -445,6 +455,10 @@ class TestFileJpeg: with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") + # RGB colorspace, no subsampling by default + im = self.roundtrip(hopper(), subsampling=3, keep_rgb=True) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + def test_exif(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 23da312a6..46998e03a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -486,6 +486,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exif** If present, the image will be stored with the provided raw EXIF data. +**keep_rgb** + By default, libjpeg converts images with an RGB color space to YCbCr. + If this option is present and true, those images will be stored as RGB + instead. + + .. versionadded:: 10.2.0 + **subsampling** If present, sets the subsampling for the encoder. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 5add65f45..00548e8b7 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -781,6 +781,7 @@ def _save(im, fp, filename): progressive, info.get("smooth", 0), optimize, + info.get("keep_rgb", False), info.get("streamtype", 0), dpi[0], dpi[1], diff --git a/src/encode.c b/src/encode.c index 4664ad0f3..c7dd51015 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1042,6 +1042,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { Py_ssize_t progressive = 0; Py_ssize_t smooth = 0; Py_ssize_t optimize = 0; + int keep_rgb = 0; Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ Py_ssize_t xdpi = 0, ydpi = 0; Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ @@ -1059,13 +1060,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnnnOz#y#y#", + "ss|nnnnpnnnnnnOz#y#y#", &mode, &rawmode, &quality, &progressive, &smooth, &optimize, + &keep_rgb, &streamtype, &xdpi, &ydpi, @@ -1150,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); + ((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb; ((JPEGENCODERSTATE *)encoder->state.context)->quality = quality; ((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays; ((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 5cc74e69b..98eaac28d 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -74,6 +74,9 @@ typedef struct { /* Optimize Huffman tables (slow) */ int optimize; + /* Disable automatic conversion of RGB images to YCbCr if nonzero */ + int keep_rgb; + /* Stream type (0=full, 1=tables only, 2=image only) */ int streamtype; diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 9da830b18..2946cc530 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -137,6 +137,20 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Compressor configuration */ jpeg_set_defaults(&context->cinfo); + /* Prevent RGB -> YCbCr conversion */ + if (context->keep_rgb) { + switch (context->cinfo.in_color_space) { + case JCS_RGB: +#ifdef JCS_EXTENSIONS + case JCS_EXT_RGBX: +#endif + jpeg_set_colorspace(&context->cinfo, JCS_RGB); + break; + default: + break; + } + } + /* Use custom quantization tables */ if (context->qtables) { int i; From f90827dfc881c1dbbebe27aed78abc279a09f0be Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Nov 2023 14:35:23 +1100 Subject: [PATCH 011/186] Rearranged subsampling assertions --- Tests/test_file_jpeg.py | 43 +++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3b11c1123..e270820bd 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -145,11 +145,13 @@ class TestFileJpeg: def getchannels(im): return tuple(v[0] for v in im.layer) - im = self.roundtrip(hopper()) - assert getchannels(im) == (1, 2, 3) - im = self.roundtrip(hopper(), keep_rgb=True) - assert getchannels(im) == (ord("R"), ord("G"), ord("B")) - assert_image_similar(hopper(), im, 12) + im = hopper() + im_ycbcr = self.roundtrip(im) + assert getchannels(im_ycbcr) == (1, 2, 3) + + im_rgb = self.roundtrip(im, keep_rgb=True) + assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B")) + assert_image_similar(im, im_rgb, 12) @pytest.mark.parametrize( "test_image_path", @@ -434,31 +436,26 @@ class TestFileJpeg: # experimental API im = self.roundtrip(hopper(), subsampling=-1) # default assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=0) # 4:4:4 - assert getsampling(im) == (1, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=1) # 4:2:2 - assert getsampling(im) == (2, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=2) # 4:2:0 - assert getsampling(im) == (2, 2, 1, 1, 1, 1) + for subsampling in (0, "4:4:4"): + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + for subsampling in (1, "4:2:2"): + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (2, 1, 1, 1, 1, 1) + for subsampling in (2, "4:2:0", "4:1:1"): + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) + im = self.roundtrip(hopper(), subsampling=3) # default (undefined) assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:4:4") - assert getsampling(im) == (1, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:2:2") - assert getsampling(im) == (2, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:2:0") - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:1:1") - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - - with pytest.raises(TypeError): - self.roundtrip(hopper(), subsampling="1:1:1") - # RGB colorspace, no subsampling by default im = self.roundtrip(hopper(), subsampling=3, keep_rgb=True) assert getsampling(im) == (1, 1, 1, 1, 1, 1) + with pytest.raises(TypeError): + self.roundtrip(hopper(), subsampling="1:1:1") + def test_exif(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() From 14146732be77093e57622cbc367311219cc64c29 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Mon, 4 Dec 2023 06:12:29 -0600 Subject: [PATCH 012/186] Clarify JPEG tests for default/invalid subsampling -1 is the default; 3 is invalid and should behave the same as the default. --- Tests/test_file_jpeg.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index e270820bd..748c07119 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -434,8 +434,9 @@ class TestFileJpeg: return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] # experimental API - im = self.roundtrip(hopper(), subsampling=-1) # default - assert getsampling(im) == (2, 2, 1, 1, 1, 1) + for subsampling in (-1, 3): # (default, invalid) + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) for subsampling in (0, "4:4:4"): im = self.roundtrip(hopper(), subsampling=subsampling) assert getsampling(im) == (1, 1, 1, 1, 1, 1) @@ -446,11 +447,8 @@ class TestFileJpeg: im = self.roundtrip(hopper(), subsampling=subsampling) assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=3) # default (undefined) - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - # RGB colorspace, no subsampling by default - im = self.roundtrip(hopper(), subsampling=3, keep_rgb=True) + im = self.roundtrip(hopper(), keep_rgb=True) assert getsampling(im) == (1, 1, 1, 1, 1, 1) with pytest.raises(TypeError): From cbab3a5c9b35dfe0f45d999705fd7b2b95838473 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 14 Dec 2023 18:57:19 +1100 Subject: [PATCH 013/186] Removed "Success" job, as this is not used in mergify --- .github/workflows/wheels.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 76d42b470..0ce689066 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -194,13 +194,3 @@ jobs: with: name: dist path: dist/*.tar.gz - - success: - permissions: - contents: none - needs: [build, windows, sdist] - runs-on: ubuntu-latest - name: Wheels Successful - steps: - - name: Success - run: echo Wheels Successful From 6693046143071b86c586544aa43f61fa600dc2ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 14 Dec 2023 19:02:48 +1100 Subject: [PATCH 014/186] Added trusted PyPI publishing --- .github/workflows/wheels.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0ce689066..3162f135c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -194,3 +194,17 @@ jobs: with: name: dist path: dist/*.tar.gz + + pypi-publish: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + needs: [build, windows, sdist] + runs-on: ubuntu-latest + name: Upload release to PyPI + environment: + name: pypi + url: https://pypi.org/p/Pillow + permissions: + id-token: write + steps: + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 From c81beb5164f4d20c842ce310ecd80ae6df18416c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 15 Dec 2023 07:55:26 +1100 Subject: [PATCH 015/186] Updated step name Co-authored-by: Hugo van Kemenade --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3162f135c..59d4bbaaf 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -206,5 +206,5 @@ jobs: permissions: id-token: write steps: - - name: Publish package distributions to PyPI + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 From 714ae0efcfaa5c92527be9a5fddd663b3c7aa47f Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 20 Dec 2023 19:37:23 +0100 Subject: [PATCH 016/186] Updated actions/upload-artifact to v4 --- .github/workflows/wheels.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5c73a5273..d18552f29 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -159,17 +159,11 @@ jobs: name: dist path: ./wheelhouse/*.whl - - name: Prepare to upload FriBiDi - run: | - mkdir fribidi\${{ matrix.arch }} - copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }} - shell: cmd - - name: Upload fribidi.dll - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: fribidi - path: fribidi\* + name: fribidi-windows-${{ matrix.arch }} + path: winbuild\build\bin\fribidi* sdist: runs-on: ubuntu-latest From 1105c745d914c2ea2f6b2ad43049f502d91dc3d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 20:29:15 +0000 Subject: [PATCH 017/186] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 67e603d0d..8e3757ca8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -76,7 +76,7 @@ def cmds_cmake( *params, '-G "{cmake_generator}"', f'-B "{build_dir}"', - f"-S .", + "-S .", ] ), f'{{cmake}} --build "{build_dir}" --clean-first --parallel --target {target}', From 808f312c7a2e3ba9354740720a4108440b51ffe5 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 15 Dec 2023 21:32:33 +1100 Subject: [PATCH 018/186] Updated environment name Co-authored-by: Hugo van Kemenade --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 59d4bbaaf..b36d7f278 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -201,7 +201,7 @@ jobs: runs-on: ubuntu-latest name: Upload release to PyPI environment: - name: pypi + name: release-pypi url: https://pypi.org/p/Pillow permissions: id-token: write From fc1cf9f65bda946eabd586c63b3d71aa1bd06043 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 20 Dec 2023 09:04:41 +1100 Subject: [PATCH 019/186] Published to Test PyPI Co-authored-by: Hugo van Kemenade --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b36d7f278..4b72f2c1d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -208,3 +208,5 @@ jobs: steps: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ From 97c2b6672c6029063121275fd544872fd81e51e1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 24 Dec 2023 13:42:57 +1100 Subject: [PATCH 020/186] Updated actions/upload-artifact to v4 --- .github/workflows/wheels.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4b72f2c1d..25520bb32 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -75,9 +75,9 @@ jobs: CIBW_TEST_SKIP: "*-macosx_arm64" MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: dist + name: dist-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.manylinux }} path: ./wheelhouse/*.whl windows: @@ -157,9 +157,9 @@ jobs: shell: cmd - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: dist + name: dist-${{ matrix.arch }} path: ./wheelhouse/*.whl - name: Prepare to upload FriBiDi @@ -171,9 +171,9 @@ jobs: - name: Upload fribidi.dll if: "matrix.arch != 'ARM64'" - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: fribidi + name: fribidi-${{ matrix.arch }} path: fribidi\* sdist: @@ -190,9 +190,9 @@ jobs: - run: make sdist - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: dist + name: dist-sdist path: dist/*.tar.gz pypi-publish: @@ -206,6 +206,11 @@ jobs: permissions: id-token: write steps: + - uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: dist + merge-multiple: true - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: From d2dc8d3a6e326866b39266958dc8515632af7cfc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 24 Dec 2023 15:59:12 +1100 Subject: [PATCH 021/186] Added type hints --- src/PIL/ImageSequence.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index e09b001e8..2c1850276 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -16,6 +16,10 @@ ## from __future__ import annotations +from typing import Callable + +from . import Image + class Iterator: """ @@ -29,14 +33,14 @@ class Iterator: :param im: An image object. """ - def __init__(self, im): + def __init__(self, im: Image.Image): if not hasattr(im, "seek"): msg = "im must have seek method" raise AttributeError(msg) self.im = im self.position = getattr(self.im, "_min_frame", 0) - def __getitem__(self, ix): + def __getitem__(self, ix: int) -> Image.Image: try: self.im.seek(ix) return self.im @@ -44,10 +48,10 @@ class Iterator: msg = "end of sequence" raise IndexError(msg) from e - def __iter__(self): + def __iter__(self) -> Iterator: return self - def __next__(self): + def __next__(self) -> Image.Image: try: self.im.seek(self.position) self.position += 1 @@ -57,7 +61,10 @@ class Iterator: raise StopIteration(msg) from e -def all_frames(im, func=None): +def all_frames( + im: Image.Image | list[Image.Image], + func: Callable[[Image.Image], Image.Image] | None = None, +) -> list[Image.Image]: """ Applies a given function to all frames in an image or a list of images. The frames are returned as a list of separate images. From 2b806c5236e2b4de1ad02e57687cca40ecaf57d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Dec 2023 07:19:00 +1100 Subject: [PATCH 022/186] Removed trailing dash --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 25520bb32..55c41b595 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -77,7 +77,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}-${{ matrix.archs }}-${{ matrix.manylinux }} + name: dist-${{ matrix.os }}-${{ matrix.archs }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl windows: From f8d75032dbbe5931d5536ee8134e164c36b8e049 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Dec 2023 18:58:47 +1100 Subject: [PATCH 023/186] Added strict type hints --- src/PIL/Image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1cb484b85..d04801cba 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1181,7 +1181,7 @@ class Image: return im - def copy(self): + def copy(self) -> Image: """ Copies this image. Use this method if you wish to paste things into an image, but still retain the original. @@ -2450,7 +2450,7 @@ class Image: if open_fp: fp.close() - def seek(self, frame): + def seek(self, frame) -> Image: """ Seeks to the given frame in this sequence file. If you seek beyond the end of the sequence, the method raises an @@ -2537,7 +2537,7 @@ class Image: return self._new(self.im.getband(channel)) - def tell(self): + def tell(self) -> int: """ Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. From d400ef2b46914c693fa94bf5188cb1cee0383735 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Dec 2023 12:18:38 +1100 Subject: [PATCH 024/186] Added type hints --- src/PIL/ContainerIO.py | 2 +- src/PIL/TarIO.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 387a4c182..64d042426 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -24,7 +24,7 @@ class ContainerIO: file (for example a TAR file). """ - def __init__(self, file, offset, length): + def __init__(self, file, offset, length) -> None: """ Create file object. diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 26522d93f..c9923487d 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -16,6 +16,7 @@ from __future__ import annotations import io +from types import TracebackType from . import ContainerIO @@ -23,7 +24,7 @@ from . import ContainerIO class TarIO(ContainerIO.ContainerIO): """A file object that provides read access to a given member of a TAR file.""" - def __init__(self, tarfile, file): + def __init__(self, tarfile: str, file: str) -> None: """ Create file object. @@ -57,11 +58,16 @@ class TarIO(ContainerIO.ContainerIO): super().__init__(self.fh, self.fh.tell(), size) # Context manager support - def __enter__(self): + def __enter__(self) -> TarIO: return self - def __exit__(self, *args): + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: self.close() - def close(self): + def close(self) -> None: self.fh.close() From 7b9d101533ea0e94e334427b7dba6a78bf5ab477 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Dec 2023 16:47:04 +1100 Subject: [PATCH 025/186] Moved __future__ import to beginning of file --- Tests/oss-fuzz/fuzz_font.py | 3 ++- Tests/oss-fuzz/fuzz_pillow.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index 4e7c7deec..024117c56 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -1,5 +1,7 @@ #!/usr/bin/python3 +from __future__ import annotations + # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,7 +15,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations import atheris diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index e7cd0474a..c1ab42e56 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -1,5 +1,7 @@ #!/usr/bin/python3 +from __future__ import annotations + # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,7 +15,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations import atheris From bc5ec2268a45795c58cd87686986194174b4dcb0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Dec 2023 22:26:11 +1100 Subject: [PATCH 026/186] Added type hints --- docs/conf.py | 2 +- docs/example/anchors.py | 2 +- selftest.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9974b0f2a..a70dece74 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -233,7 +233,7 @@ htmlhelp_basename = "PillowPILForkdoc" # -- Options for LaTeX output --------------------------------------------- -latex_elements = { +latex_elements: dict[str, str] = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). diff --git a/docs/example/anchors.py b/docs/example/anchors.py index 3a0e40b84..b5d76b4fe 100644 --- a/docs/example/anchors.py +++ b/docs/example/anchors.py @@ -5,7 +5,7 @@ from PIL import Image, ImageDraw, ImageFont font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16) -def test(anchor): +def test(anchor: str) -> Image.Image: im = Image.new("RGBA", (200, 100), "white") d = ImageDraw.Draw(im) d.line(((100, 0), (100, 100)), "gray") diff --git a/selftest.py b/selftest.py index 600fd6496..ed5252c44 100755 --- a/selftest.py +++ b/selftest.py @@ -15,7 +15,7 @@ except AttributeError: pass -def testimage(): +def testimage() -> None: """ PIL lets you create in-memory images with various pixel types: From 26f58e7afc679adaa96d1478a148362f1d1b71a3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 22 Dec 2023 16:30:45 +0200 Subject: [PATCH 027/186] Optimise ImageMode.getmode --- src/PIL/ImageMode.py | 111 +++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index d61dd6fea..501be16e0 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -15,9 +15,7 @@ from __future__ import annotations import sys - -# mode descriptor cache -_modes = None +from functools import lru_cache class ModeDescriptor: @@ -41,58 +39,59 @@ class ModeDescriptor: return self.mode +@lru_cache def getmode(mode: str) -> ModeDescriptor: """Gets a mode descriptor for the given mode.""" - global _modes - if not _modes: - # initialize mode cache - modes = {} - endian = "<" if sys.byteorder == "little" else ">" - for m, (basemode, basetype, bands, typestr) in { - # core modes - # Bits need to be extended to bytes - "1": ("L", "L", ("1",), "|b1"), - "L": ("L", "L", ("L",), "|u1"), - "I": ("L", "I", ("I",), endian + "i4"), - "F": ("L", "F", ("F",), endian + "f4"), - "P": ("P", "L", ("P",), "|u1"), - "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), - "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), - "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), - # UNDONE - unsigned |u1i1i1 - "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), - "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), - # extra experimental modes - "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), - "LA": ("L", "L", ("L", "A"), "|u1"), - "La": ("L", "L", ("L", "a"), "|u1"), - "PA": ("RGB", "L", ("P", "A"), "|u1"), - }.items(): - modes[m] = ModeDescriptor(m, bands, basemode, basetype, typestr) - # mapping modes - for i16mode, typestr in { - # I;16 == I;16L, and I;32 == I;32L - "I;16": "u2", - "I;16BS": ">i2", - "I;16N": endian + "u2", - "I;16NS": endian + "i2", - "I;32": "u4", - "I;32L": "i4", - "I;32LS": "" + + modes = { + # core modes + # Bits need to be extended to bytes + "1": ("L", "L", ("1",), "|b1"), + "L": ("L", "L", ("L",), "|u1"), + "I": ("L", "I", ("I",), endian + "i4"), + "F": ("L", "F", ("F",), endian + "f4"), + "P": ("P", "L", ("P",), "|u1"), + "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), + "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), + "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), + # UNDONE - unsigned |u1i1i1 + "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), + "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), + # extra experimental modes + "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), + "LA": ("L", "L", ("L", "A"), "|u1"), + "La": ("L", "L", ("L", "a"), "|u1"), + "PA": ("RGB", "L", ("P", "A"), "|u1"), + } + if mode in modes: + base_mode, base_type, bands, type_str = modes[mode] + return ModeDescriptor(mode, bands, base_mode, base_type, type_str) + + mapping_modes = { + # I;16 == I;16L, and I;32 == I;32L + "I;16": "u2", + "I;16BS": ">i2", + "I;16N": endian + "u2", + "I;16NS": endian + "i2", + "I;32": "u4", + "I;32L": "i4", + "I;32LS": " Date: Mon, 4 Dec 2023 07:31:24 -0600 Subject: [PATCH 028/186] Fail if chroma subsampling selected when writing RGB JPEG The user presumably doesn't intend to subsample the green and blue channels. --- Tests/test_file_jpeg.py | 12 +++++++++--- docs/handbook/image-file-formats.rst | 3 +++ src/libImaging/JpegEncode.c | 10 ++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 748c07119..75851e477 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -447,9 +447,15 @@ class TestFileJpeg: im = self.roundtrip(hopper(), subsampling=subsampling) assert getsampling(im) == (2, 2, 1, 1, 1, 1) - # RGB colorspace, no subsampling by default - im = self.roundtrip(hopper(), keep_rgb=True) - assert getsampling(im) == (1, 1, 1, 1, 1, 1) + # RGB colorspace + for subsampling in (-1, 0, "4:4:4"): + # "4:4:4" doesn't really make sense for RGB, but the conversion + # to an integer happens at a higher level + im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + for subsampling in (1, "4:2:2", 2, "4:2:0", 3): + with pytest.raises(OSError): + self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 46998e03a..773d517ef 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -491,6 +491,9 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If this option is present and true, those images will be stored as RGB instead. + When this option is enabled, attempting to chroma-subsample RGB images + with the ``subsampling`` option will raise an :py:exc:`OSError`. + .. versionadded:: 10.2.0 **subsampling** diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 2946cc530..00f3d5f74 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -144,6 +144,16 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { #ifdef JCS_EXTENSIONS case JCS_EXT_RGBX: #endif + switch (context->subsampling) { + case -1: /* Default */ + case 0: /* No subsampling */ + break; + default: + /* Would subsample the green and blue + channels, which doesn't make sense */ + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } jpeg_set_colorspace(&context->cinfo, JCS_RGB); break; default: From e2018a6697d5e1cbee4eb29da887f8fc9b0b60f4 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Wed, 13 Dec 2023 18:27:55 -0600 Subject: [PATCH 029/186] Add release note for JPEG keep_rgb option Co-authored-by: Andrew Murray --- docs/releasenotes/10.2.0.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 9883f10ba..284de7f4f 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -46,6 +46,14 @@ Added DdsImagePlugin enums :py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT` enums have been added to :py:class:`PIL.DdsImagePlugin`. +JPEG RGB color space +^^^^^^^^^^^^^^^^^^^^ + +When saving JPEG files, ``keep_rgb`` can now be set to ``True``. This will store RGB +images in the RGB color space instead of being converted to YCbCr automatically by +libjpeg. When this option is enabled, attempting to chroma-subsample RGB images with +the ``subsampling`` option will raise an :py:exc:`OSError`. + JPEG restart marker interval ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From a72b5963d73c90017bd3ab457df52256d8812eff Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Mon, 11 Dec 2023 04:14:18 -0600 Subject: [PATCH 030/186] Document JPEG 2000 support for writing YCbCr and reading subsampled color Read support for subsampled RGB and YCbCr color channels was added in 4f4c3b34f8 and not documented at the time. Write support for YCbCr appears to date to 61fb89ec54, the original commit. Retain the existing language about YCbCr input conversion to RGB, even though it's not completely correct. OpenJPEG through 2.5.0 doesn't set color_space in opj_read_header(), so we end up in our OPJ_CLRSPC_UNSPECIFIED fallback path, which guesses sRGB if there's no component subsampling. This means we currently can't round-trip YCbCr via JPEG 2000. The next OpenJPEG release will fix this, so leave the docs as is. Also fix typo: .j2p -> .jp2. --- docs/handbook/image-file-formats.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 9cd65fd48..53565dbbd 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -552,12 +552,13 @@ JPEG 2000 .. versionadded:: 2.4.0 -Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB`` or -``RGBA`` data. It can also read files containing ``YCbCr`` data, which it -converts on read into ``RGB`` or ``RGBA`` depending on whether or not there is -an alpha channel. Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), -as well as boxed JPEG 2000 files (``.j2p`` or ``.jpx`` files). Pillow does -*not* support files whose components have different sampling frequencies. +Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, +``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to +``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. +Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``, +``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports +JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files +(``.jp2`` or ``.jpx`` files). When loading, if you set the ``mode`` on the image prior to the :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to From a66fbb95b6239c20ca9d6a42ca5b8448fc461984 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 27 Dec 2023 07:54:03 +1100 Subject: [PATCH 031/186] Updated Windows artifact name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c73cdb2c6..6149aa1ec 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -156,7 +156,7 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.arch }} + name: dist-windows-${{ matrix.arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll From 043e13d660f1000e3a8ba3f1e84fd8e2b9ddd135 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 08:12:51 +1100 Subject: [PATCH 032/186] Revert "Published to Test PyPI" This reverts commit fc1cf9f65bda946eabd586c63b3d71aa1bd06043. --- .github/workflows/wheels.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6149aa1ec..060fc497e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -202,5 +202,3 @@ jobs: merge-multiple: true - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/ From 396b96f28b1ea3037720a5279c85cf0fdeb5a015 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 26 Dec 2023 15:08:58 -0700 Subject: [PATCH 033/186] Optimise ImageMode.getmode Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageMode.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 501be16e0..0b31f6081 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -42,7 +42,6 @@ class ModeDescriptor: @lru_cache def getmode(mode: str) -> ModeDescriptor: """Gets a mode descriptor for the given mode.""" - # if not _modes: # initialize mode cache endian = "<" if sys.byteorder == "little" else ">" From e9252a9353561d54a5554c2bb873c3264be26add Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 11:07:53 +1100 Subject: [PATCH 034/186] Always return None from compile() --- src/PIL/FontFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 9621770e2..04616929a 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -65,7 +65,7 @@ class FontFile: ysize = lines * h if xsize == 0 and ysize == 0: - return "" + return self.ysize = h From 85818cd61677cb51c5c70e2ffc2ba7626408ba4b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 11:36:16 +1100 Subject: [PATCH 035/186] Added type hints to FontFile and subclasses --- src/PIL/BdfFontFile.py | 21 ++++++++++++++----- src/PIL/FontFile.py | 46 ++++++++++++++++++++++++++++++++---------- src/PIL/Image.py | 10 ++++----- src/PIL/PcfFontFile.py | 31 +++++++++++++++------------- src/PIL/_binary.py | 6 +++--- 5 files changed, 76 insertions(+), 38 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index b12ddc2d4..e3eda4fe9 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -22,6 +22,8 @@ Parse X Bitmap Distribution Format (BDF) """ from __future__ import annotations +from typing import BinaryIO + from . import FontFile, Image bdf_slant = { @@ -36,7 +38,17 @@ bdf_slant = { bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} -def bdf_char(f): +def bdf_char( + f: BinaryIO, +) -> ( + tuple[ + str, + int, + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]], + Image.Image, + ] + | None +): # skip to STARTCHAR while True: s = f.readline() @@ -56,13 +68,12 @@ def bdf_char(f): props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") # load bitmap - bitmap = [] + bitmap = bytearray() while True: s = f.readline() if not s or s[:7] == b"ENDCHAR": break - bitmap.append(s[:-1]) - bitmap = b"".join(bitmap) + bitmap += s[:-1] # The word BBX # followed by the width in x (BBw), height in y (BBh), @@ -92,7 +103,7 @@ def bdf_char(f): class BdfFontFile(FontFile.FontFile): """Font file plugin for the X11 BDF format.""" - def __init__(self, fp): + def __init__(self, fp: BinaryIO): super().__init__() s = fp.readline() diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 04616929a..9f6437779 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -16,13 +16,16 @@ from __future__ import annotations import os +from typing import BinaryIO from . import Image, _binary WIDTH = 800 -def puti16(fp, values): +def puti16( + fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int] +) -> None: """Write network order (big-endian) 16-bit sequence""" for v in values: if v < 0: @@ -33,16 +36,34 @@ def puti16(fp, values): class FontFile: """Base class for raster font file handlers.""" - bitmap = None + bitmap: Image.Image | None = None - def __init__(self): - self.info = {} - self.glyph = [None] * 256 + def __init__(self) -> None: + self.info: dict[bytes, bytes | int] = {} + self.glyph: list[ + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ] = [None] * 256 - def __getitem__(self, ix): + def __getitem__( + self, ix: int + ) -> ( + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ): return self.glyph[ix] - def compile(self): + def compile(self) -> None: """Create metrics and bitmap""" if self.bitmap: @@ -51,7 +72,7 @@ class FontFile: # create bitmap large enough to hold all data h = w = maxwidth = 0 lines = 1 - for glyph in self: + for glyph in self.glyph: if glyph: d, dst, src, im = glyph h = max(h, src[3] - src[1]) @@ -71,7 +92,10 @@ class FontFile: # paste glyphs into bitmap self.bitmap = Image.new("1", (xsize, ysize)) - self.metrics = [None] * 256 + self.metrics: list[ + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]] + | None + ] = [None] * 256 x = y = 0 for i in range(256): glyph = self[i] @@ -88,7 +112,7 @@ class FontFile: self.bitmap.paste(im.crop(src), s) self.metrics[i] = d, dst, s - def save(self, filename): + def save(self, filename: str) -> None: """Save font""" self.compile() @@ -104,6 +128,6 @@ class FontFile: for id in range(256): m = self.metrics[id] if not m: - puti16(fp, [0] * 10) + puti16(fp, (0,) * 10) else: puti16(fp, m[0] + m[1] + m[2]) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d04801cba..045a06080 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1194,7 +1194,7 @@ class Image: __copy__ = copy - def crop(self, box=None): + def crop(self, box=None) -> Image: """ Returns a rectangular region from this image. The box is a 4-tuple defining the left, upper, right, and lower pixel @@ -1659,7 +1659,7 @@ class Image: return self.im.entropy(extrema) return self.im.entropy() - def paste(self, im, box=None, mask=None): + def paste(self, im, box=None, mask=None) -> None: """ Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the @@ -2352,7 +2352,7 @@ class Image: (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor ) - def save(self, fp, format=None, **params): + def save(self, fp, format=None, **params) -> None: """ Saves this image under the given filename. If no format is specified, the format to use is determined from the filename @@ -2903,7 +2903,7 @@ def _check_size(size): return True -def new(mode, size, color=0): +def new(mode, size, color=0) -> Image: """ Creates a new image with the given mode and size. @@ -2942,7 +2942,7 @@ def new(mode, size, color=0): return im._new(core.fill(mode, size, color)) -def frombytes(mode, size, data, decoder_name="raw", *args): +def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: """ Creates a copy of an image memory from pixel data in a buffer. diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index d602a1633..eff846c74 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -18,6 +18,7 @@ from __future__ import annotations import io +from typing import BinaryIO, Callable from . import FontFile, Image from ._binary import i8 @@ -49,7 +50,7 @@ BYTES_PER_ROW = [ ] -def sz(s, o): +def sz(s: bytes, o: int) -> bytes: return s[o : s.index(b"\0", o)] @@ -58,7 +59,7 @@ class PcfFontFile(FontFile.FontFile): name = "name" - def __init__(self, fp, charset_encoding="iso8859-1"): + def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"): self.charset_encoding = charset_encoding magic = l32(fp.read(4)) @@ -104,7 +105,9 @@ class PcfFontFile(FontFile.FontFile): bitmaps[ix], ) - def _getformat(self, tag): + def _getformat( + self, tag: int + ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]: format, size, offset = self.toc[tag] fp = self.fp @@ -119,7 +122,7 @@ class PcfFontFile(FontFile.FontFile): return fp, format, i16, i32 - def _load_properties(self): + def _load_properties(self) -> dict[bytes, bytes | int]: # # font properties @@ -138,18 +141,16 @@ class PcfFontFile(FontFile.FontFile): data = fp.read(i32(fp.read(4))) for k, s, v in p: - k = sz(data, k) - if s: - v = sz(data, v) - properties[k] = v + property_value: bytes | int = sz(data, v) if s else v + properties[sz(data, k)] = property_value return properties - def _load_metrics(self): + def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]: # # font metrics - metrics = [] + metrics: list[tuple[int, int, int, int, int, int, int, int]] = [] fp, format, i16, i32 = self._getformat(PCF_METRICS) @@ -182,7 +183,9 @@ class PcfFontFile(FontFile.FontFile): return metrics - def _load_bitmaps(self, metrics): + def _load_bitmaps( + self, metrics: list[tuple[int, int, int, int, int, int, int, int]] + ) -> list[Image.Image]: # # bitmap data @@ -207,7 +210,7 @@ class PcfFontFile(FontFile.FontFile): data = fp.read(bitmapsize) - pad = BYTES_PER_ROW[padindex] + pad: Callable[[int], int] = BYTES_PER_ROW[padindex] mode = "1;R" if bitorder: mode = "1" @@ -222,7 +225,7 @@ class PcfFontFile(FontFile.FontFile): return bitmaps - def _load_encoding(self): + def _load_encoding(self) -> list[int | None]: fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) first_col, last_col = i16(fp.read(2)), i16(fp.read(2)) @@ -233,7 +236,7 @@ class PcfFontFile(FontFile.FontFile): nencoding = (last_col - first_col + 1) * (last_row - first_row + 1) # map character code to bitmap index - encoding = [None] * min(256, nencoding) + encoding: list[int | None] = [None] * min(256, nencoding) encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)] diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index c60c9cec1..9bb4260a4 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -18,7 +18,7 @@ from __future__ import annotations from struct import pack, unpack_from -def i8(c): +def i8(c) -> int: return c if c.__class__ is int else c[0] @@ -57,7 +57,7 @@ def si16be(c, o=0): return unpack_from(">h", c, o)[0] -def i32le(c, o=0): +def i32le(c, o=0) -> int: """ Converts a 4-bytes (32 bits) string to an unsigned integer. @@ -94,7 +94,7 @@ def o32le(i): return pack(" bytes: return pack(">H", i) From 6e97dd5cecc074a0d8622e4aa5658b4263324760 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 12:32:35 +1100 Subject: [PATCH 036/186] Raise ValueError when trying to save without bitmap --- Tests/test_fontfile.py | 12 ++++++++++++ src/PIL/FontFile.py | 3 +++ 2 files changed, 15 insertions(+) create mode 100644 Tests/test_fontfile.py diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py new file mode 100644 index 000000000..ce1e02f63 --- /dev/null +++ b/Tests/test_fontfile.py @@ -0,0 +1,12 @@ +from __future__ import annotations +import pytest + +from PIL import FontFile + + +def test_save(tmp_path): + tempname = str(tmp_path / "temp.pil") + + font = FontFile.FontFile() + with pytest.raises(ValueError): + font.save(tempname) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 9f6437779..3ec1ae819 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -118,6 +118,9 @@ class FontFile: self.compile() # font data + if not self.bitmap: + msg = "No bitmap created" + raise ValueError(msg) self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") # font metrics From 0aebd577ea08b668f738a735b55fb6c69e8c9332 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 Dec 2023 22:27:20 +1100 Subject: [PATCH 037/186] Moved type hint to BYTES_PER_ROW --- src/PIL/PcfFontFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index eff846c74..0d1968b14 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -42,7 +42,7 @@ PCF_SWIDTHS = 1 << 6 PCF_GLYPH_NAMES = 1 << 7 PCF_BDF_ACCELERATORS = 1 << 8 -BYTES_PER_ROW = [ +BYTES_PER_ROW: list[Callable[[int], int]] = [ lambda bits: ((bits + 7) >> 3), lambda bits: ((bits + 15) >> 3) & ~1, lambda bits: ((bits + 31) >> 3) & ~3, @@ -210,7 +210,7 @@ class PcfFontFile(FontFile.FontFile): data = fp.read(bitmapsize) - pad: Callable[[int], int] = BYTES_PER_ROW[padindex] + pad = BYTES_PER_ROW[padindex] mode = "1;R" if bitorder: mode = "1" From 6bcf807fe2cb82c859fb757a498edfe5692ededa Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 00:17:57 +0100 Subject: [PATCH 038/186] add type hints for _util --- pyproject.toml | 3 +++ src/PIL/_util.py | 23 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 193e8c9b2..0c25edeb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ classifiers = [ dynamic = [ "version", ] +dependencies = [ + 'typing-extensions; python_version < "3.10"', +] [project.optional-dependencies] docs = [ "furo", diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 4634d335b..d11cbfa58 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,21 +1,36 @@ from __future__ import annotations import os +import sys from pathlib import Path +from typing import Any, NoReturn + +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + from typing_extensions import TypeGuard -def is_path(f): +def is_path(f: Any) -> TypeGuard[bytes | str | Path]: return isinstance(f, (bytes, str, Path)) -def is_directory(f): +def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: """Checks if an object is a string, and that it points to a directory.""" return is_path(f) and os.path.isdir(f) class DeferredError: - def __init__(self, ex): + def __init__(self, ex: BaseException): self.ex = ex - def __getattr__(self, elt): + def __getattr__(self, elt: str) -> NoReturn: raise self.ex + + @staticmethod + def new(ex: BaseException) -> Any: + """ + Creates an object that raises the wrapped exception ``ex`` when used, + and casts it to :py:obj:`~typing.Any` type. + """ + return DeferredError(ex) From 90d5552800692d3c9c467eeef406dab984a50e68 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 00:40:55 +0100 Subject: [PATCH 039/186] use _util.DeferredError.new everywhere --- Tests/test_util.py | 2 +- pyproject.toml | 2 -- src/PIL/Image.py | 2 +- src/PIL/ImageCms.py | 2 +- src/PIL/ImageFont.py | 6 +++--- src/PIL/PyAccess.py | 2 +- src/PIL/_imagingcms.pyi | 5 +++++ src/PIL/_imagingft.pyi | 5 +++++ 8 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 src/PIL/_imagingcms.pyi create mode 100644 src/PIL/_imagingft.pyi diff --git a/Tests/test_util.py b/Tests/test_util.py index 1457d85f7..4a312beb4 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -66,7 +66,7 @@ def test_deferred_error(): # Arrange # Act - thing = _util.DeferredError(ValueError("Some error text")) + thing = _util.DeferredError.new(ValueError("Some error text")) # Assert with pytest.raises(ValueError): diff --git a/pyproject.toml b/pyproject.toml index 0c25edeb3..ef0ff8a8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,9 +144,7 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageCms.py$', '^src/PIL/ImageFile.py$', - '^src/PIL/ImageFont.py$', '^src/PIL/ImageMath.py$', '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d04801cba..5a2e85419 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -92,7 +92,7 @@ try: raise ImportError(msg) except ImportError as v: - core = DeferredError(ImportError("The _imaging C module is not installed.")) + core = DeferredError.new(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 9d27f2513..643fce830 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -28,7 +28,7 @@ except ImportError as ex: # anything in core. from ._util import DeferredError - _imagingcms = DeferredError(ex) + _imagingcms = DeferredError.new(ex) DESCRIPTION = """ pyCMS diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6db7cc4ec..41d8fbc17 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,7 +34,7 @@ import warnings from enum import IntEnum from io import BytesIO from pathlib import Path -from typing import IO +from typing import BinaryIO from . import Image from ._util import is_directory, is_path @@ -53,7 +53,7 @@ try: except ImportError as ex: from ._util import DeferredError - core = DeferredError(ex) + core = DeferredError.new(ex) def _string_length_check(text): @@ -191,7 +191,7 @@ class FreeTypeFont: def __init__( self, - font: bytes | str | Path | IO | None = None, + font: bytes | str | Path | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 23ff154f6..07bb712d8 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -43,7 +43,7 @@ except ImportError as ex: # anything in core. from ._util import DeferredError - FFI = ffi = DeferredError(ex) + FFI = ffi = DeferredError.new(ex) logger = logging.getLogger(__name__) diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imagingcms.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imagingft.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... From cc51dace35bd6e144c3fff05d1b14c78af41c0e0 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 00:41:30 +0100 Subject: [PATCH 040/186] fix types hints for ImageFile._Tile --- pyproject.toml | 1 - src/PIL/ImageFile.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ef0ff8a8e..de707a283 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,6 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageFile.py$', '^src/PIL/ImageMath.py$', '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index ae4e23db1..0923979af 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -32,7 +32,7 @@ import io import itertools import struct import sys -from typing import NamedTuple +from typing import Any, NamedTuple from . import Image from ._deprecate import deprecate @@ -94,7 +94,7 @@ class _Tile(NamedTuple): encoder_name: str extents: tuple[int, int, int, int] offset: int - args: tuple | str | None + args: tuple[Any, ...] | str | None # From 3a4298d16c660d5dec46cfa372b6aceb79c1e056 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 14:54:48 +0100 Subject: [PATCH 041/186] avoid hard dependency on typing_extensions --- docs/reference/internal_modules.rst | 13 +++++++++++++ pyproject.toml | 6 +++--- src/PIL/_typing.py | 16 ++++++++++++++++ src/PIL/_util.py | 7 +------ tox.ini | 2 ++ 5 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 src/PIL/_typing.py diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 363a67d9b..57dd3f35b 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -25,6 +25,19 @@ Internal Modules :undoc-members: :show-inheritance: +:mod:`~PIL._typing` Module +-------------------------- + +.. module:: PIL._typing + +Provides a convenient way to import type hints that are not available +on some supported Python versions. + +.. py:data:: TypeGuard + :value: typing.TypeGuard + + See :py:obj:`typing.TypeGuard`. + :mod:`~PIL._util` Module ------------------------ diff --git a/pyproject.toml b/pyproject.toml index de707a283..abb32f14d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,6 @@ classifiers = [ dynamic = [ "version", ] -dependencies = [ - 'typing-extensions; python_version < "3.10"', -] [project.optional-dependencies] docs = [ "furo", @@ -68,6 +65,9 @@ tests = [ "pytest-cov", "pytest-timeout", ] +typing = [ + 'typing-extensions; python_version < "3.10"', +] xmp = [ "defusedxml", ] diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py new file mode 100644 index 000000000..583be41b7 --- /dev/null +++ b/src/PIL/_typing.py @@ -0,0 +1,16 @@ +import sys + +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + try: + from typing_extensions import TypeGuard + except ImportError: + from typing import Any, Type + + class TypeGuard: # type: ignore[no-redef] + def __class_getitem__(cls, item: Any) -> Type[bool]: + return bool + + +__all__ = ["TypeGuard"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index d11cbfa58..37cd979f6 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,14 +1,9 @@ from __future__ import annotations import os -import sys from pathlib import Path from typing import Any, NoReturn - -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: - from typing_extensions import TypeGuard +from ._typing import TypeGuard def is_path(f: Any) -> TypeGuard[bytes | str | Path]: diff --git a/tox.ini b/tox.ini index 034d89372..bfcfaf5d9 100644 --- a/tox.ini +++ b/tox.ini @@ -37,3 +37,5 @@ deps = numpy commands = mypy src {posargs} +extras = + typing From 0d90bc818789b801170171fe902bc2921850fc11 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Dec 2023 13:57:20 +0000 Subject: [PATCH 042/186] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/_typing.py | 6 ++++-- src/PIL/_util.py | 1 + tox.ini | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 583be41b7..608b2b41f 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys if sys.version_info >= (3, 10): @@ -6,10 +8,10 @@ else: try: from typing_extensions import TypeGuard except ImportError: - from typing import Any, Type + from typing import Any class TypeGuard: # type: ignore[no-redef] - def __class_getitem__(cls, item: Any) -> Type[bool]: + def __class_getitem__(cls, item: Any) -> type[bool]: return bool diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 37cd979f6..13f369cca 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -3,6 +3,7 @@ from __future__ import annotations import os from pathlib import Path from typing import Any, NoReturn + from ._typing import TypeGuard diff --git a/tox.ini b/tox.ini index bfcfaf5d9..d89d017e4 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,7 @@ skip_install = true deps = mypy==1.7.1 numpy -commands = - mypy src {posargs} extras = typing +commands = + mypy src {posargs} From 0c767f0d7c2da77edb984f927de6d42848a5fe23 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 5 Oct 2023 21:27:18 +0300 Subject: [PATCH 043/186] Coverage: Don't complain about code that shouldn't run: def create_lut(): --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index f71b6b1a2..d7095dce3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,6 +9,8 @@ exclude_lines = # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: + # Don't complain about code that shouldn't run + def create_lut(): # Don't complain about debug code if DEBUG: From 5938423c63f9a71bed4c88c3c1d4d1c9b51a0de0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 12 Nov 2023 12:53:42 +0200 Subject: [PATCH 044/186] Coverage: Use exclude_also instead of exclude_lines --- .coveragerc | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.coveragerc b/.coveragerc index d7095dce3..1c5e7654e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,11 +2,8 @@ [report] # Regexes for lines to exclude from consideration -exclude_lines = - # Have to re-enable the standard pragma: - pragma: no cover - - # Don't complain if non-runnable code isn't run: +exclude_also = + # Don't complain if non-runnable code isn't run if 0: if __name__ == .__main__.: # Don't complain about code that shouldn't run From 9475c46d30ddbefc74afd185ac7f19e6da8a8ca7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 27 Dec 2023 16:58:42 +0200 Subject: [PATCH 045/186] Don't complain about compatibility code: class TypeGuard --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 1c5e7654e..f18c5ea52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,8 @@ exclude_also = def create_lut(): # Don't complain about debug code if DEBUG: + # Don't complain about compatibility code + class TypeGuard: [run] omit = From de381d0efb96096cb9a56c69711635a8e53417b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Wed, 27 Dec 2023 16:17:51 +0100 Subject: [PATCH 046/186] Update docs/reference/internal_modules.rst Co-authored-by: Hugo van Kemenade --- docs/reference/internal_modules.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 57dd3f35b..f2932c322 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -31,7 +31,7 @@ Internal Modules .. module:: PIL._typing Provides a convenient way to import type hints that are not available -on some supported Python versions. +on some Python versions. .. py:data:: TypeGuard :value: typing.TypeGuard From 30015f6236ee3165e06b78ff7dced956e21e24d5 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 27 Dec 2023 16:16:25 +0100 Subject: [PATCH 047/186] simplify decompression bomb check in FreeTypeFont.render --- src/PIL/ImageFont.py | 17 +++-------------- src/_imagingft.c | 14 ++++---------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6db7cc4ec..c8d834f91 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -582,22 +582,13 @@ class FreeTypeFont: _string_length_check(text) if start is None: start = (0, 0) - im = None - size = None def fill(width, height): - nonlocal im, size - size = (width, height) - if Image.MAX_IMAGE_PIXELS is not None: - pixels = max(1, width) * max(1, height) - if pixels > 2 * Image.MAX_IMAGE_PIXELS: - return + Image._decompression_bomb_check(size) + return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - return im - - offset = self.font.render( + return self.font.render( text, fill, mode, @@ -610,8 +601,6 @@ class FreeTypeFont: start[0], start[1], ) - Image._decompression_bomb_check(size) - return im, offset def font_variant( self, font=None, size=None, index=None, encoding=None, layout_engine=None diff --git a/src/_imagingft.c b/src/_imagingft.c index 68c66ac2c..6e24fcf95 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { image = PyObject_CallFunction(fill, "ii", width, height); if (image == Py_None) { PyMem_Del(glyph_info); - return Py_BuildValue("ii", 0, 0); + return Py_BuildValue("N(ii)", image, 0, 0); } else if (image == NULL) { PyMem_Del(glyph_info); return NULL; @@ -894,7 +894,7 @@ font_render(FontObject *self, PyObject *args) { y_offset -= stroke_width; if (count == 0 || width == 0 || height == 0) { PyMem_Del(glyph_info); - return Py_BuildValue("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); } if (stroke_width) { @@ -1130,18 +1130,12 @@ font_render(FontObject *self, PyObject *args) { if (bitmap_converted_ready) { FT_Bitmap_Done(library, &bitmap_converted); } - Py_DECREF(image); FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - return Py_BuildValue("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); glyph_error: - if (im->destroy) { - im->destroy(im); - } - if (im->image) { - free(im->image); - } + Py_DECREF(image); if (stroker != NULL) { FT_Done_Glyph(glyph); } From a16974e24028667330975d61ea0229481af5e5d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Dec 2023 21:07:16 +1100 Subject: [PATCH 048/186] Restored testing of ImageFont class --- Tests/test_imagefont.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6e04cddc7..efe523643 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1053,11 +1053,11 @@ def test_too_many_characters(font): with pytest.raises(ValueError): transposed_font.getlength("A" * 1_000_001) - default_font = ImageFont.load_default() + imagefont = ImageFont.ImageFont() with pytest.raises(ValueError): - default_font.getlength("A" * 1_000_001) + imagefont.getlength("A" * 1_000_001) with pytest.raises(ValueError): - default_font.getbbox("A" * 1_000_001) + imagefont.getbbox("A" * 1_000_001) @pytest.mark.parametrize( From 372083c59f06c37564be040a1d4b8cd1dc5bd0ac Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Thu, 28 Dec 2023 13:00:17 -0600 Subject: [PATCH 049/186] Check similarity of round-tripped YCbCr JPEG, for symmetry with RGB --- Tests/test_file_jpeg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 75851e477..5513b1403 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -148,6 +148,7 @@ class TestFileJpeg: im = hopper() im_ycbcr = self.roundtrip(im) assert getchannels(im_ycbcr) == (1, 2, 3) + assert_image_similar(im, im_ycbcr, 17) im_rgb = self.roundtrip(im, keep_rgb=True) assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B")) From a5e42107eadd9e1cdcaf075782338f72b9142e32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 11:23:34 +1100 Subject: [PATCH 050/186] Removed __future__ import from fuzz_font and fuzz_pillow --- Tests/oss-fuzz/fuzz_font.py | 2 -- Tests/oss-fuzz/fuzz_pillow.py | 2 -- pyproject.toml | 2 ++ 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index 024117c56..bc2ba9a7e 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -1,7 +1,5 @@ #!/usr/bin/python3 -from __future__ import annotations - # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index c1ab42e56..545daccb6 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -1,7 +1,5 @@ #!/usr/bin/python3 -from __future__ import annotations - # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/pyproject.toml b/pyproject.toml index 193e8c9b2..6e26ff4f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,8 @@ extend-ignore = [ [tool.ruff.per-file-ignores] "Tests/*.py" = ["I001"] +"Tests/oss-fuzz/fuzz_font.py" = ["I002"] +"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] [tool.ruff.isort] known-first-party = ["PIL"] From f6bcf4e1ae3184ff9ed0cfcb46a0aa80423da537 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 14:15:40 +1100 Subject: [PATCH 051/186] Use IMAGEWIDTH and IMAGELENGTH when calculating strip size --- Tests/test_file_tiff.py | 25 +++++++++++++++++++++++-- Tests/test_file_tiff_metadata.py | 1 + src/PIL/TiffImagePlugin.py | 13 +++++++------ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 0851796d0..73a1223d7 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -484,13 +484,13 @@ class TestFileTiff: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() - exif[256] = 100 + exif[264] = 100 im.save(outfile, exif=exif) with Image.open(outfile) as im: exif = im.getexif() - assert exif[256] == 100 + assert exif[264] == 100 def test_reload_exif_after_seek(self): with Image.open("Tests/images/multipage.tiff") as im: @@ -773,6 +773,27 @@ class TestFileTiff: 4001, ] + def test_tiff_chunks(self, tmp_path): + tmpfile = str(tmp_path / "temp.tif") + + im = hopper() + with open(tmpfile, "wb") as fp: + for y in range(0, 128, 32): + chunk = im.crop((0, y, 128, y + 32)) + if y == 0: + chunk.save( + fp, + "TIFF", + tiffinfo={ + TiffImagePlugin.IMAGEWIDTH: 128, + TiffImagePlugin.IMAGELENGTH: 128, + }, + ) + else: + fp.write(chunk.tobytes()) + + assert_image_equal_tofile(im, tmpfile) + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index edd57e6b5..ed90031fa 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -162,6 +162,7 @@ def test_change_stripbytecounts_tag_type(tmp_path): # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT im = im.resize((500, 500)) + info[TiffImagePlugin.IMAGEWIDTH] = im.width # STRIPBYTECOUNTS can be a SHORT or a LONG info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index fc242ca64..f9da3e649 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1704,25 +1704,26 @@ def _save(im, fp, filename): colormap += [0] * (256 - colors) ifd[COLORMAP] = colormap # data orientation - stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) + w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] + stride = len(bits) * ((w * bits[0] + 7) // 8) # aim for given strip size (64 KB by default) when using libtiff writer if libtiff: im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) - rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) + rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) # JPEG encoder expects multiple of 8 rows if compression == "jpeg": - rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) + rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) else: - rows_per_strip = im.size[1] + rows_per_strip = h if rows_per_strip == 0: rows_per_strip = 1 strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip - strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip + strips_per_image = (h + rows_per_strip - 1) // rows_per_strip ifd[ROWSPERSTRIP] = rows_per_strip if strip_byte_counts >= 2**16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( - stride * im.size[1] - strip_byte_counts * (strips_per_image - 1), + stride * h - strip_byte_counts * (strips_per_image - 1), ) ifd[STRIPOFFSETS] = tuple( range(0, strip_byte_counts * strips_per_image, strip_byte_counts) From 3c7603b57dc60cd6a052bfb4d1fbf1c5ec7b6ff8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 22:05:31 +1100 Subject: [PATCH 052/186] Trusted PyPI publishing is now in use in GitHub Actions --- RELEASING.md | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 74f427f03..b3fd72a52 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -20,12 +20,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. git tag 5.2.0 git push --tags ``` -* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) -* [ ] Check and upload all source and binary distributions e.g.: - ```bash - python3 -m twine check --strict dist/* - python3 -m twine upload dist/Pillow-5.2.0* - ``` +* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: @@ -55,12 +50,7 @@ Released as needed for security, installation or critical bug fixes. ```bash make sdist ``` -* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) -* [ ] Check and upload all source and binary distributions e.g.: - ```bash - python3 -m twine check --strict dist/* - python3 -m twine upload dist/Pillow-5.2.1* - ``` +* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: ```bash git push @@ -82,11 +72,7 @@ Released as needed privately to individual vendors for critical security-related git tag 2.5.3 git push origin --tags ``` -* [ ] Create and check source distribution: - ```bash - make sdist - ``` -* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) +* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: ```bash git push origin 2.5.x @@ -94,14 +80,15 @@ Released as needed privately to individual vendors for critical security-related ## Source and Binary Distributions -* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) - and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): - ```bash - gh run download --dir dist - # select dist - ``` +* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) + has passed, including the "Upload release to PyPI" job. This will have been triggered + by the new tag. * [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) - and copy into `dist`. + and copy into `dist`. Check and upload them e.g.: + ```bash + python3 -m twine check --strict dist/* + python3 -m twine upload dist/Pillow-5.2.0* + ``` ## Publicize Release From 9c7ff4c86d2be8750fb33f2372c1c9bc9c20e835 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 29 Dec 2023 13:25:54 +0200 Subject: [PATCH 053/186] Add 'Type hints' as a release note category --- .github/release-drafter.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 4d855469a..3711d91f0 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -13,6 +13,8 @@ categories: label: "Removal" - title: "Testing" label: "Testing" + - title: "Type hints" + label: "Type hints" exclude-labels: - "changelog: skip" From f7ec665bf1656245e9e3b483ff92e9eb1e0ef465 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Dec 2023 22:59:43 +1100 Subject: [PATCH 054/186] Support setting ROWSPERSTRIP tag --- Tests/test_file_tiff.py | 8 ++++++++ Tests/test_file_tiff_metadata.py | 2 ++ src/PIL/TiffImagePlugin.py | 31 +++++++++++++++++-------------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 0851796d0..0737c8a2a 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -612,6 +612,14 @@ class TestFileTiff: assert_image_equal_tofile(im, tmpfile) + def test_rowsperstrip(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + im = hopper() + im.save(outfile, tiffinfo={278: 256}) + + with Image.open(outfile) as im: + assert im.tag_v2[278] == 256 + def test_strip_raw(self): infile = "Tests/images/tiff_strip_raw.tif" with Image.open(infile) as im: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index edd57e6b5..1a814d97e 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -123,6 +123,7 @@ def test_write_metadata(tmp_path): """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: f = str(tmp_path / "temp.tiff") + del img.tag[278] img.save(f, tiffinfo=img.tag) original = img.tag_v2.named() @@ -159,6 +160,7 @@ def test_change_stripbytecounts_tag_type(tmp_path): out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.tif") as im: info = im.tag_v2 + del info[278] # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT im = im.resize((500, 500)) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index fc242ca64..e9db64e5f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1705,20 +1705,23 @@ def _save(im, fp, filename): ifd[COLORMAP] = colormap # data orientation stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) - # aim for given strip size (64 KB by default) when using libtiff writer - if libtiff: - im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) - rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) - # JPEG encoder expects multiple of 8 rows - if compression == "jpeg": - rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) - else: - rows_per_strip = im.size[1] - if rows_per_strip == 0: - rows_per_strip = 1 - strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip - strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip - ifd[ROWSPERSTRIP] = rows_per_strip + if ROWSPERSTRIP not in ifd: + # aim for given strip size (64 KB by default) when using libtiff writer + if libtiff: + im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) + rows_per_strip = ( + 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) + ) + # JPEG encoder expects multiple of 8 rows + if compression == "jpeg": + rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) + else: + rows_per_strip = im.size[1] + if rows_per_strip == 0: + rows_per_strip = 1 + ifd[ROWSPERSTRIP] = rows_per_strip + strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] + strips_per_image = (im.size[1] + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] if strip_byte_counts >= 2**16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( From 6a33d6d170d51419ea63e905637c98f5e6db1af9 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 29 Dec 2023 23:15:41 +0100 Subject: [PATCH 055/186] add type hints to PIL._binary --- src/PIL/_binary.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 9bb4260a4..b8b871df6 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -18,16 +18,16 @@ from __future__ import annotations from struct import pack, unpack_from -def i8(c) -> int: - return c if c.__class__ is int else c[0] +def i8(c: int | bytes) -> int: + return c if c.__class__ is int else c[0] # type: ignore[index, return-value] -def o8(i): +def o8(i: int) -> bytes: return bytes((i & 255,)) # Input, le = little endian, be = big endian -def i16le(c, o=0): +def i16le(c: bytes, o: int = 0) -> int: """ Converts a 2-bytes (16 bits) string to an unsigned integer. @@ -37,7 +37,7 @@ def i16le(c, o=0): return unpack_from(" int: """ Converts a 2-bytes (16 bits) string to a signed integer. @@ -47,7 +47,7 @@ def si16le(c, o=0): return unpack_from(" int: """ Converts a 2-bytes (16 bits) string to a signed integer, big endian. @@ -57,7 +57,7 @@ def si16be(c, o=0): return unpack_from(">h", c, o)[0] -def i32le(c, o=0) -> int: +def i32le(c: bytes, o: int = 0) -> int: """ Converts a 4-bytes (32 bits) string to an unsigned integer. @@ -67,7 +67,7 @@ def i32le(c, o=0) -> int: return unpack_from(" int: """ Converts a 4-bytes (32 bits) string to a signed integer. @@ -77,26 +77,26 @@ def si32le(c, o=0): return unpack_from(" int: return unpack_from(">H", c, o)[0] -def i32be(c, o=0): +def i32be(c: bytes, o: int = 0) -> int: return unpack_from(">I", c, o)[0] # Output, le = little endian, be = big endian -def o16le(i): +def o16le(i: int) -> bytes: return pack(" bytes: return pack(" bytes: +def o16be(i: int) -> bytes: return pack(">H", i) -def o32be(i): +def o32be(i: int) -> bytes: return pack(">I", i) From 9a6b6316a781b27c9f845fd71c6e0290e8b4601b Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 29 Dec 2023 23:12:30 +0100 Subject: [PATCH 056/186] add type hints to PIL.ContainerIO --- src/PIL/ContainerIO.py | 23 ++++++++++++----------- src/PIL/TarIO.py | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 64d042426..0035296a4 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -16,15 +16,16 @@ from __future__ import annotations import io +from typing import IO, AnyStr, Generic, Literal -class ContainerIO: +class ContainerIO(Generic[AnyStr]): """ A file object that provides read access to a part of an existing file (for example a TAR file). """ - def __init__(self, file, offset, length) -> None: + def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None: """ Create file object. @@ -32,7 +33,7 @@ class ContainerIO: :param offset: Start of region, in bytes. :param length: Size of region, in bytes. """ - self.fh = file + self.fh: IO[AnyStr] = file self.pos = 0 self.offset = offset self.length = length @@ -41,10 +42,10 @@ class ContainerIO: ## # Always false. - def isatty(self): + def isatty(self) -> bool: return False - def seek(self, offset, mode=io.SEEK_SET): + def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: """ Move file pointer. @@ -63,7 +64,7 @@ class ContainerIO: self.pos = max(0, min(self.pos, self.length)) self.fh.seek(self.offset + self.pos) - def tell(self): + def tell(self) -> int: """ Get current file pointer. @@ -71,7 +72,7 @@ class ContainerIO: """ return self.pos - def read(self, n=0): + def read(self, n: int = 0) -> AnyStr: """ Read data. @@ -84,17 +85,17 @@ class ContainerIO: else: n = self.length - self.pos if not n: # EOF - return b"" if "b" in self.fh.mode else "" + return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] self.pos = self.pos + n return self.fh.read(n) - def readline(self): + def readline(self) -> AnyStr: """ Read a line of text. :returns: An 8-bit string. """ - s = b"" if "b" in self.fh.mode else "" + s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] newline_character = b"\n" if "b" in self.fh.mode else "\n" while True: c = self.read(1) @@ -105,7 +106,7 @@ class ContainerIO: break return s - def readlines(self): + def readlines(self) -> list[AnyStr]: """ Read multiple lines of text. diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index c9923487d..7470663b4 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -21,7 +21,7 @@ from types import TracebackType from . import ContainerIO -class TarIO(ContainerIO.ContainerIO): +class TarIO(ContainerIO.ContainerIO[bytes]): """A file object that provides read access to a given member of a TAR file.""" def __init__(self, tarfile: str, file: str) -> None: From 45c726fd4daa63236a8f3653530f297dc87b160a Mon Sep 17 00:00:00 2001 From: Eric Soroos Date: Fri, 27 Oct 2023 11:21:18 +0200 Subject: [PATCH 057/186] Don't allow __ or builtins in env dictionarys for ImageMath.eval --- src/PIL/ImageMath.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 7ca512e75..cf108e258 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -237,6 +237,10 @@ def eval(expression, _dict={}, **kw): args.update(_dict) args.update(kw) for k, v in args.items(): + if '__' in k or hasattr(__builtins__, k): + msg = f"'{k}' not allowed" + raise ValueError(msg) + if hasattr(v, "im"): args[k] = _Operand(v) From 0ca3c33c59927e1c7e0c14dbc1eea1dfb2431a80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Oct 2023 15:58:52 +1100 Subject: [PATCH 058/186] Allow ops --- Tests/test_imagemath.py | 5 +++++ src/PIL/ImageMath.py | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 22de86c7c..9a0326ece 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -64,6 +64,11 @@ def test_prevent_exec(expression): ImageMath.eval(expression) +def test_prevent_double_underscores(): + with pytest.raises(ValueError): + ImageMath.eval("1", {"__": None}) + + def test_logical(): assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index cf108e258..fd7d78d45 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -234,13 +234,14 @@ def eval(expression, _dict={}, **kw): # build execution namespace args = ops.copy() - args.update(_dict) - args.update(kw) - for k, v in args.items(): - if '__' in k or hasattr(__builtins__, k): + for k in list(_dict.keys()) + list(kw.keys()): + if "__" in k or hasattr(__builtins__, k): msg = f"'{k}' not allowed" raise ValueError(msg) + args.update(_dict) + args.update(kw) + for k, v in args.items(): if hasattr(v, "im"): args[k] = _Operand(v) From 557ba59d13de919d04b3fd4cdef8634f7d4b3348 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Dec 2023 09:30:12 +1100 Subject: [PATCH 059/186] Include further builtins --- Tests/test_imagemath.py | 5 +++++ docs/releasenotes/10.2.0.rst | 9 ++++++--- src/PIL/ImageMath.py | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 9a0326ece..9281de6f6 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -69,6 +69,11 @@ def test_prevent_double_underscores(): ImageMath.eval("1", {"__": None}) +def test_prevent_builtins(): + with pytest.raises(ValueError): + ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None}) + + def test_logical(): assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 9883f10ba..ade152fcd 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -62,10 +62,13 @@ output only the quantization and Huffman tables for the image. Security ======== -TODO -^^^^ +Restricted environment keys for ImageMath.eval +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +:cve:`2023-50447`: If an attacker has control over the keys passed to the +``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute +arbitrary code. To prevent this, keys matching the names of builtins and keys +containing double underscores will now raise a :py:exc:`ValueError`. Other Changes ============= diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index fd7d78d45..b77f4bce5 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -235,7 +235,7 @@ def eval(expression, _dict={}, **kw): # build execution namespace args = ops.copy() for k in list(_dict.keys()) + list(kw.keys()): - if "__" in k or hasattr(__builtins__, k): + if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" raise ValueError(msg) From aaf99d18ae3e25369b1cd4cebcf51e20a9b6d1ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Dec 2023 10:38:09 +1100 Subject: [PATCH 060/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index df4e11e0e..c5c3ea9c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Restricted environment keys for ImageMath.eval() #7655 + [wiredfool, radarhere] + - Fix incorrect color blending for overlapping glyphs #7497 [ZachNagengast, nulano, radarhere] From 9158c9aec029bb4aaa2fca7040f04ff030536a0f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 26 Dec 2023 19:03:18 +0200 Subject: [PATCH 061/186] Optimise ImageColor using functools.lru_cache --- src/PIL/ImageColor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index bfad27c82..ad59b0667 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -19,10 +19,12 @@ from __future__ import annotations import re +from functools import lru_cache from . import Image +@lru_cache def getrgb(color): """ Convert a color string to an RGB or RGBA tuple. If the string cannot be @@ -121,6 +123,7 @@ def getrgb(color): raise ValueError(msg) +@lru_cache def getcolor(color, mode): """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if From ee6e12a8032a8255b2f7a60a40c50377c17a03fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 18:37:16 +1100 Subject: [PATCH 062/186] Updated copyright year --- LICENSE | 2 +- docs/COPYING | 2 +- docs/conf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index cf65e86d7..0069eb5bc 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors. + Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors. Like PIL, Pillow is licensed under the open source HPND License: diff --git a/docs/COPYING b/docs/COPYING index bc44ba388..73af6d99c 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors + Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/conf.py b/docs/conf.py index a70dece74..9ae7ae605 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,7 +54,7 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" copyright = ( - "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" + "1995-2011 Fredrik Lundh, 2010-2024 Jeffrey A. Clark (Alex) and contributors" ) author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors" From 1d9c931626b3cd08b9fa19c9e9e2d8c79604046e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 22:43:08 +1100 Subject: [PATCH 063/186] Changed tile tuple to match other plugins --- Tests/test_file_iptc.py | 9 +++++++++ src/PIL/IptcImagePlugin.py | 10 +++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index d0ecde393..e1a8c92c7 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -11,6 +11,15 @@ from .helper import hopper TEST_FILE = "Tests/images/iptc.jpg" +def test_open(): + f = BytesIO( + b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c" + b"\x03\x14\x00\x01\x01\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x00" + ) + with Image.open(f) as im: + assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] + + def test_getiptcinfo_jpg_none(): # Arrange with hopper() as im: diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index e7dc3e4e4..09a60f25c 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -128,24 +128,20 @@ class IptcImageFile(ImageFile.ImageFile): # tile if tag == (8, 10): - self.tile = [ - ("iptc", (compression, offset), (0, 0, self.size[0], self.size[1])) - ] + self.tile = [("iptc", (0, 0) + self.size, offset, compression)] def load(self): if len(self.tile) != 1 or self.tile[0][0] != "iptc": return ImageFile.ImageFile.load(self) - type, tile, box = self.tile[0] - - encoding, offset = tile + offset, compression = self.tile[0][2:] self.fp.seek(offset) # Copy image data to temporary file o_fd, outfile = tempfile.mkstemp(text=False) o = os.fdopen(o_fd) - if encoding == "raw": + if compression == "raw": # To simplify access to the extracted file, # prepend a PPM header o.write("P5\n%d %d\n255\n" % self.size) From 2ec53e36e9135fda4e2eb6bbd5343a7facae71da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 23:17:28 +1100 Subject: [PATCH 064/186] Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() --- Tests/test_imagefont.py | 2 ++ docs/releasenotes/10.2.0.rst | 15 +++++++++++++-- src/PIL/ImageFont.py | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index efe523643..807d581ed 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1058,6 +1058,8 @@ def test_too_many_characters(font): imagefont.getlength("A" * 1_000_001) with pytest.raises(ValueError): imagefont.getbbox("A" * 1_000_001) + with pytest.raises(ValueError): + imagefont.getmask("A" * 1_000_001) @pytest.mark.parametrize( diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index ade152fcd..6ab139b56 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -62,8 +62,19 @@ output only the quantization and Huffman tables for the image. Security ======== -Restricted environment keys for ImageMath.eval -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +ImageFont.getmask: Applied ImageFont.MAX_STRING_LENGTH +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using arbitrary strings as text input, +Pillow will now raise a :py:exc:`ValueError` if the number of characters passed into +:py:meth:`PIL.ImageFont.ImageFont.getmask` is over a certain limit, +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. + +This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It +can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. + +ImageMath.eval: Restricted environment keys +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :cve:`2023-50447`: If an attacker has control over the keys passed to the ``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6db7cc4ec..7f0366ddb 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -149,6 +149,7 @@ class ImageFont: :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ + _string_length_check(text) return self.font.getmask(text, mode) def getbbox(self, text, *args, **kwargs): From 46a6ddf0c2eb36a06f9952a4e8d98ff2183198f6 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 31 Dec 2023 13:47:37 +0100 Subject: [PATCH 065/186] fix loading IPTC images and add test --- Tests/test_file_iptc.py | 9 ++++++--- src/PIL/IptcImagePlugin.py | 21 ++++++--------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index e1a8c92c7..04313ed85 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -6,18 +6,21 @@ import pytest from PIL import Image, IptcImagePlugin -from .helper import hopper +from .helper import assert_image_equal, hopper TEST_FILE = "Tests/images/iptc.jpg" def test_open(): + expected = Image.new("L", (1, 1), 0) + f = BytesIO( - b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c" - b"\x03\x14\x00\x01\x01\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x00" + b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01" + b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00" ) with Image.open(f) as im: assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] + assert_image_equal(im, expected) def test_getiptcinfo_jpg_none(): diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 09a60f25c..7b6123c66 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -16,8 +16,7 @@ # from __future__ import annotations -import os -import tempfile +from io import BytesIO from . import Image, ImageFile from ._binary import i8, o8 @@ -139,12 +138,11 @@ class IptcImageFile(ImageFile.ImageFile): self.fp.seek(offset) # Copy image data to temporary file - o_fd, outfile = tempfile.mkstemp(text=False) - o = os.fdopen(o_fd) + o = BytesIO() if compression == "raw": # To simplify access to the extracted file, # prepend a PPM header - o.write("P5\n%d %d\n255\n" % self.size) + o.write(b"P5\n%d %d\n255\n" % self.size) while True: type, size = self.field() if type != (8, 10): @@ -155,17 +153,10 @@ class IptcImageFile(ImageFile.ImageFile): break o.write(s) size -= len(s) - o.close() - try: - with Image.open(outfile) as _im: - _im.load() - self.im = _im.im - finally: - try: - os.unlink(outfile) - except OSError: - pass + with Image.open(o) as _im: + _im.load() + self.im = _im.im Image.register_open(IptcImageFile.format, IptcImageFile) From f410ec4eab314653bcbc91a9d0ab754fa331972e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 00:21:25 +1100 Subject: [PATCH 066/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c5c3ea9c7..1dc8e9aaa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,9 +5,18 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662 + [radarhere] + +- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657 + [hugovk] + - Restricted environment keys for ImageMath.eval() #7655 [wiredfool, radarhere] +- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641 + [hugovk, radarhere] + - Fix incorrect color blending for overlapping glyphs #7497 [ZachNagengast, nulano, radarhere] From b1e88ac17fcde6286a9b50262ca945fbf90652c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sun, 31 Dec 2023 14:49:48 +0100 Subject: [PATCH 067/186] omit default color value Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_iptc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 04313ed85..075a461af 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -12,7 +12,7 @@ TEST_FILE = "Tests/images/iptc.jpg" def test_open(): - expected = Image.new("L", (1, 1), 0) + expected = Image.new("L", (1, 1)) f = BytesIO( b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01" From 3396ce102d0a2e8db663522001cab9786edf19c0 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 29 Dec 2023 23:18:08 +0100 Subject: [PATCH 068/186] do not accept int in PIL._binary.i8 --- src/PIL/IptcImagePlugin.py | 15 +++++++++------ src/PIL/_binary.py | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index e7dc3e4e4..cc39441b6 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -20,26 +20,29 @@ import os import tempfile from . import Image, ImageFile -from ._binary import i8, o8 from ._binary import i16be as i16 from ._binary import i32be as i32 COMPRESSION = {1: "raw", 5: "jpeg"} -PAD = o8(0) * 4 +PAD = b"\0\0\0\0" # # Helpers +def _i8(c: int | bytes) -> int: + return c if isinstance(c, int) else c[0] + + def i(c): return i32((PAD + c)[-4:]) def dump(c): for i in c: - print("%02x" % i8(i), end=" ") + print("%02x" % _i8(i), end=" ") print() @@ -103,10 +106,10 @@ class IptcImageFile(ImageFile.ImageFile): self.info[tag] = tagdata # mode - layers = i8(self.info[(3, 60)][0]) - component = i8(self.info[(3, 60)][1]) + layers = self.info[(3, 60)][0] + component = self.info[(3, 60)][1] if (3, 65) in self.info: - id = i8(self.info[(3, 65)][0]) - 1 + id = self.info[(3, 65)][0] - 1 else: id = 0 if layers == 1 and not component: diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index b8b871df6..0a07e8d0e 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -18,8 +18,8 @@ from __future__ import annotations from struct import pack, unpack_from -def i8(c: int | bytes) -> int: - return c if c.__class__ is int else c[0] # type: ignore[index, return-value] +def i8(c: bytes) -> int: + return c[0] def o8(i: int) -> bytes: From fa4b3776f0dda812a1cea15f3ba17a3777b8c5b6 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 30 Dec 2023 00:02:34 +0100 Subject: [PATCH 069/186] =?UTF-8?q?=EF=BB=BFdeprecate=20IptcImagePlugin.{d?= =?UTF-8?q?ump,i,PAD}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/test_file_iptc.py | 20 ++++++++++++-------- src/PIL/IptcImagePlugin.py | 21 +++++++++++++++++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index e1a8c92c7..3960e027a 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -87,24 +87,28 @@ def test_i(): c = b"a" # Act - ret = IptcImagePlugin.i(c) + with pytest.warns(DeprecationWarning): + ret = IptcImagePlugin.i(c) # Assert assert ret == 97 -def test_dump(): +def test_dump(monkeypatch): # Arrange c = b"abc" # Temporarily redirect stdout - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() + mystdout = StringIO() + monkeypatch.setattr(sys, "stdout", mystdout) # Act - IptcImagePlugin.dump(c) - - # Reset stdout - sys.stdout = old_stdout + with pytest.warns(DeprecationWarning): + IptcImagePlugin.dump(c) # Assert assert mystdout.getvalue() == "61 62 63 \n" + + +def test_pad_deprecation(): + with pytest.warns(DeprecationWarning): + assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 3a028de2d..deac39e25 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -22,25 +22,38 @@ import tempfile from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._deprecate import deprecate COMPRESSION = {1: "raw", 5: "jpeg"} -PAD = b"\0\0\0\0" + +def __getattr__(name): + if name == "PAD": + deprecate("IptcImagePlugin.PAD", 12) + return b"\0\0\0\0" + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) # # Helpers +def _i(c): + return i32((b"\0\0\0\0" + c)[-4:]) + + def _i8(c: int | bytes) -> int: return c if isinstance(c, int) else c[0] def i(c): - return i32((PAD + c)[-4:]) + deprecate("IptcImagePlugin.i", 12) + return _i(c) def dump(c): + deprecate("IptcImagePlugin.dump", 12) for i in c: print("%02x" % _i8(i), end=" ") print() @@ -56,7 +69,7 @@ class IptcImageFile(ImageFile.ImageFile): format_description = "IPTC/NAA" def getint(self, key): - return i(self.info[key]) + return _i(self.info[key]) def field(self): # @@ -80,7 +93,7 @@ class IptcImageFile(ImageFile.ImageFile): elif size == 128: size = 0 elif size > 128: - size = i(self.fp.read(size - 128)) + size = _i(self.fp.read(size - 128)) else: size = i16(s, 3) From aa605bc6f2c1baa00d95e083a3be2fe0b5051c04 Mon Sep 17 00:00:00 2001 From: Nulano Date: Sun, 31 Dec 2023 01:25:19 +0100 Subject: [PATCH 070/186] =?UTF-8?q?=EF=BB=BFdocument=20IptcImagePlugin=20d?= =?UTF-8?q?eprecations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/deprecations.rst | 11 +++++++++++ docs/releasenotes/10.2.0.rst | 10 +++++++--- src/PIL/IptcImagePlugin.py | 2 ++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 75c0b73eb..a42dc555f 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -44,6 +44,17 @@ ImageFile.raise_oserror error codes returned by a codec's ``decode()`` method, which ImageFile already does automatically. +IptcImageFile helper functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.2.0 + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). These are undocumented helper functions intended +for internal use, so there is no replacement. They can each be replaced +by a single line of code using builtin functions in Python. + Removed features ---------------- diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 6ab139b56..0244b6f77 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -20,10 +20,14 @@ ImageFile.raise_oserror error codes returned by a codec's ``decode()`` method, which ImageFile already does automatically. -TODO -^^^^ +IptcImageFile helper functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). These are undocumented helper functions intended +for internal use, so there is no replacement. They can each be replaced +by a single line of code using builtin functions in Python. API Changes =========== diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index deac39e25..faf3ed936 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -48,11 +48,13 @@ def _i8(c: int | bytes) -> int: def i(c): + """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.i", 12) return _i(c) def dump(c): + """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.dump", 12) for i in c: print("%02x" % _i8(i), end=" ") From e1ea522f706ac51f4bdd6b58724af56eebd19d66 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 13:21:56 +0100 Subject: [PATCH 071/186] =?UTF-8?q?=EF=BB=BFAdded=20further=20type=20hints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/PIL/Image.py | 6 +++--- src/PIL/IptcImagePlugin.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 613d9462a..055dbda7a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3190,7 +3190,7 @@ def _decompression_bomb_check(size): ) -def open(fp, mode="r", formats=None): +def open(fp, mode="r", formats=None) -> Image: """ Opens and identifies the given image file. @@ -3415,7 +3415,7 @@ def merge(mode, bands): # Plugin registry -def register_open(id, factory, accept=None): +def register_open(id, factory, accept=None) -> None: """ Register an image file plugin. This function should not be used in application code. @@ -3469,7 +3469,7 @@ def register_save_all(id, driver): SAVE_ALL[id.upper()] = driver -def register_extension(id, extension): +def register_extension(id, extension) -> None: """ Registers an image extension. This function should not be used in application code. diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index faf3ed936..2314ddce8 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import os import tempfile +from typing import Sequence from . import Image, ImageFile from ._binary import i16be as i16 @@ -27,7 +28,7 @@ from ._deprecate import deprecate COMPRESSION = {1: "raw", 5: "jpeg"} -def __getattr__(name): +def __getattr__(name: str) -> bytes: if name == "PAD": deprecate("IptcImagePlugin.PAD", 12) return b"\0\0\0\0" @@ -39,7 +40,7 @@ def __getattr__(name): # Helpers -def _i(c): +def _i(c: bytes) -> int: return i32((b"\0\0\0\0" + c)[-4:]) @@ -47,13 +48,13 @@ def _i8(c: int | bytes) -> int: return c if isinstance(c, int) else c[0] -def i(c): +def i(c: bytes) -> int: """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.i", 12) return _i(c) -def dump(c): +def dump(c: Sequence[int | bytes]) -> None: """.. deprecated:: 10.2.0""" deprecate("IptcImagePlugin.dump", 12) for i in c: @@ -70,10 +71,10 @@ class IptcImageFile(ImageFile.ImageFile): format = "IPTC" format_description = "IPTC/NAA" - def getint(self, key): + def getint(self, key: tuple[int, int]) -> int: return _i(self.info[key]) - def field(self): + def field(self) -> tuple[tuple[int, int] | None, int]: # # get a IPTC field header s = self.fp.read(5) @@ -101,7 +102,7 @@ class IptcImageFile(ImageFile.ImageFile): return tag, size - def _open(self): + def _open(self) -> None: # load descriptive fields while True: offset = self.fp.tell() From 2825323e84aff1c6005506fb1d6e78373a356486 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 31 Dec 2023 16:46:24 +0200 Subject: [PATCH 072/186] Release notes: add ImageColor and ImageMode optimisations, and type hints --- docs/PIL.rst | 9 ++++++++ docs/releasenotes/10.2.0.rst | 40 ++++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/docs/PIL.rst b/docs/PIL.rst index fa036b9cc..493d0bde3 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -69,6 +69,15 @@ can be found here. :undoc-members: :show-inheritance: +:mod:`~PIL.ImageMode` Module +----------------------------- + +.. automodule:: PIL.ImageMode + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: + :mod:`~PIL.ImageTransform` Module --------------------------------- diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 6ab139b56..f82d20176 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -1,14 +1,6 @@ 10.2.0 ------ -Backwards Incompatible Changes -============================== - -TODO -^^^^ - -TODO - Deprecations ============ @@ -20,11 +12,6 @@ ImageFile.raise_oserror error codes returned by a codec's ``decode()`` method, which ImageFile already does automatically. -TODO -^^^^ - -TODO - API Changes =========== @@ -92,6 +79,21 @@ Support has been added to read the BC4U format of DDS images. Support has also been added to read DX10 BC1 and BC4, whether UNORM or TYPELESS. +Optimized ImageColor.getrgb and getcolor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The color calculations of :py:attr:`~PIL.ImageColor.getrgb` and +:py:attr:`~PIL.ImageColor.getcolor` are now cached using +:py:func:`functools.lru_cache`. Cached calls of ``getrgb`` are 3.1 - 91.4 times +as fast and ``getcolor`` are 5.1 - 19.6 times as fast. + +Optimized ImageMode.getmode +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using +:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 20% +faster. + Optimized ImageStat.Stat count and extrema ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -105,3 +107,15 @@ Encoder errors now report error detail as string :py:exc:`OSError` exceptions from image encoders now include a textual description of the error instead of a numeric error code. + +Type hints +^^^^^^^^^^ + +Work has begun to add type annotations to Pillow, including: + +* :py:class:`~PIL.ContainerIO` +* :py:class:`~PIL.FontFile` and subclasses +* :py:class:`~PIL.ImageChops` +* :py:class:`~PIL.ImageMode` +* :py:class:`~PIL.ImageSequence` +* :py:class:`~PIL.TarIO` From 129a4936d33f775e2122b5f21d49ceeeb601e0b5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 31 Dec 2023 09:53:59 -0700 Subject: [PATCH 073/186] Update docs/releasenotes/10.2.0.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- docs/releasenotes/10.2.0.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index f82d20176..c08a85c2c 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -113,9 +113,9 @@ Type hints Work has begun to add type annotations to Pillow, including: -* :py:class:`~PIL.ContainerIO` -* :py:class:`~PIL.FontFile` and subclasses -* :py:class:`~PIL.ImageChops` -* :py:class:`~PIL.ImageMode` -* :py:class:`~PIL.ImageSequence` -* :py:class:`~PIL.TarIO` +* :py:mod:`~PIL.ContainerIO` +* :py:mod:`~PIL.FontFile` and subclasses +* :py:mod:`~PIL.ImageChops` +* :py:mod:`~PIL.ImageMode` +* :py:mod:`~PIL.ImageSequence` +* :py:mod:`~PIL.TarIO` From b7d64ac177e641085baea239b8b4f164d143db8e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 31 Dec 2023 10:30:22 -0700 Subject: [PATCH 074/186] Don't complain about compatibility code for missing optional dependencies --- .coveragerc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index f18c5ea52..70dd4e57f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,8 +10,8 @@ exclude_also = def create_lut(): # Don't complain about debug code if DEBUG: - # Don't complain about compatibility code - class TypeGuard: + # Don't complain about compatibility code for missing optional dependencies + except ImportError [run] omit = From d26880cda9af73377233040ae14783ea20b0af51 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 31 Dec 2023 20:06:40 +0200 Subject: [PATCH 075/186] Remove unused create_lut() --- .coveragerc | 2 -- Tests/test_imagemorph.py | 9 --------- 2 files changed, 11 deletions(-) diff --git a/.coveragerc b/.coveragerc index 70dd4e57f..46df3f90d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,8 +6,6 @@ exclude_also = # Don't complain if non-runnable code isn't run if 0: if __name__ == .__main__.: - # Don't complain about code that shouldn't run - def create_lut(): # Don't complain about debug code if DEBUG: # Don't complain about compatibility code for missing optional dependencies diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index ec55aadf9..64a1785ea 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -57,15 +57,6 @@ def test_str_to_img(): assert_image_equal_tofile(A, "Tests/images/morph_a.png") -def create_lut(): - for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): - lb = ImageMorph.LutBuilder(op_name=op) - lut = lb.build_lut() - with open(f"Tests/images/{op}.lut", "wb") as f: - f.write(lut) - - -# create_lut() @pytest.mark.parametrize( "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") ) From 9e835ca5be86818bdfce583cd4f544f2cb741db5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 13:51:45 +1100 Subject: [PATCH 076/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1dc8e9aaa..2aeae2d5c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,21 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Concatenate multiple JPEG EXIF markers #7496 + [radarhere] + +- Changed IPTC tile tuple to match other plugins #7661 + [radarhere] + +- Do not assign new fp attribute when exiting context manager #7566 + [radarhere] + +- Support arbitrary masks for uncompressed RGB DDS images #7589 + [radarhere, akx] + +- Support setting ROWSPERSTRIP tag #7654 + [radarhere] + - Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662 [radarhere] From 9bcd4770582ecd0e66479671e271c91b5926a80b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 14:19:16 +1100 Subject: [PATCH 077/186] Use consistent language --- docs/releasenotes/10.2.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index c08a85c2c..46cfe1599 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -91,16 +91,16 @@ Optimized ImageMode.getmode ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using -:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 20% -faster. +:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 1.2 times as +fast. Optimized ImageStat.Stat count and extrema ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and :py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the -histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3x as fast -on average and ``st.extrema`` is 12x as fast on average. +histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on +average and ``st.extrema`` is 12 times as fast on average. Encoder errors now report error detail as string ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From ca48ac0e15075a0a986826f094c2b68aa8f4ac26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 14:31:07 +1100 Subject: [PATCH 078/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 2aeae2d5c..6da7b28d4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Allow uncompressed TIFF images to be saved in chunks #7650 + [radarhere] + - Concatenate multiple JPEG EXIF markers #7496 [radarhere] From 17911d6ec4de6a19142bb49897876684f6f5000f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 14:49:01 +1100 Subject: [PATCH 079/186] Removed import --- src/PIL/IptcImagePlugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 7b6123c66..6b5e34c4f 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -172,8 +172,6 @@ def getiptcinfo(im): :returns: A dictionary containing IPTC information, or None if no IPTC information block was found. """ - import io - from . import JpegImagePlugin, TiffImagePlugin data = None @@ -208,7 +206,7 @@ def getiptcinfo(im): # parse the IPTC information chunk im.info = {} - im.fp = io.BytesIO(data) + im.fp = BytesIO(data) try: im._open() From 2c75cac4002bffca9691af7c28732ed51cc85929 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 15:25:29 +1100 Subject: [PATCH 080/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6da7b28d4..6062ad742 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Deprecate IptcImagePlugin helpers #7664 + [nulano, hugovk, radarhere] + - Allow uncompressed TIFF images to be saved in chunks #7650 [radarhere] From 8422af20d583112156749e3519d3796cf6a1312b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 15:47:53 +1100 Subject: [PATCH 081/186] Removed unnecessary "pragma: no cover" --- src/PIL/ImageFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 021b40c0e..035b83c4d 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -396,7 +396,7 @@ class Color3DLUT(MultibandFilter): if hasattr(table, "shape"): try: import numpy - except ImportError: # pragma: no cover + except ImportError: pass if numpy and isinstance(table, numpy.ndarray): From af026fdd3ce08c4865467fe3343dc8d7360af8da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:06:09 +1100 Subject: [PATCH 082/186] Added decompression bomb check to ImageFont.getmask() --- src/PIL/ImageFont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7f0366ddb..78c6b9eca 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -150,6 +150,7 @@ class ImageFont: :py:mod:`PIL.Image.core` interface module. """ _string_length_check(text) + Image._decompression_bomb_check(self.font.getsize(text)) return self.font.getmask(text, mode) def getbbox(self, text, *args, **kwargs): From 8676cbd4e7adafc0b5972ab51fed7b06ab8719d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:13:24 +1100 Subject: [PATCH 083/186] Do not try and crop glyphs from outside of source ImageFont image --- docs/releasenotes/10.2.0.rst | 10 ++++++++++ src/_imaging.c | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 0244b6f77..4e7c58756 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -77,6 +77,16 @@ Pillow will now raise a :py:exc:`ValueError` if the number of characters passed This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. +A decompression bomb check has also been added to +:py:meth:`PIL.ImageFont.ImageFont.getmask`. + +ImageFont.getmask: Trim glyph size +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using PIL fonts, +:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that +they do not extend beyond the bitmap image. + ImageMath.eval: Restricted environment keys ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/_imaging.c b/src/_imaging.c index 2270c77fe..e06780c75 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2649,6 +2649,18 @@ _font_new(PyObject *self_, PyObject *args) { self->glyphs[i].sy0 = S16(B16(glyphdata, 14)); self->glyphs[i].sx1 = S16(B16(glyphdata, 16)); self->glyphs[i].sy1 = S16(B16(glyphdata, 18)); + + // Do not allow glyphs to extend beyond bitmap image + // Helps prevent DOS by stopping cropped images being larger than the original + if (self->glyphs[i].sx1 > self->bitmap->xsize) { + self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize; + self->glyphs[i].sx1 = self->bitmap->xsize; + } + if (self->glyphs[i].sy1 > self->bitmap->ysize) { + self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize; + self->glyphs[i].sy1 = self->bitmap->ysize; + } + if (self->glyphs[i].dy0 < y0) { y0 = self->glyphs[i].dy0; } From ecd3948b45ca9c4c2e8b0b3cb58fe6af9798cfa2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:03:43 +1100 Subject: [PATCH 084/186] Test PILfont even when FreeType is supported --- Tests/test_imagefontpil.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 21b4dee3c..fd07ee23b 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,14 +1,22 @@ from __future__ import annotations +import struct import pytest +from io import BytesIO -from PIL import Image, ImageDraw, ImageFont, features +from PIL import Image, ImageDraw, ImageFont, features, _util from .helper import assert_image_equal_tofile -pytestmark = pytest.mark.skipif( - features.check_module("freetype2"), - reason="PILfont superseded if FreeType is supported", -) +original_core = ImageFont.core + + +def setup_module(): + if features.check_module("freetype2"): + ImageFont.core = _util.DeferredError(ImportError) + + +def teardown_module(): + ImageFont.core = original_core def test_default_font(): @@ -44,3 +52,23 @@ def test_textbbox(): default_font = ImageFont.load_default() assert d.textlength("test", font=default_font) == 24 assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) + + +def test_decompression_bomb(): + glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (256, 256))) + with pytest.raises(Image.DecompressionBombError): + font.getmask("A" * 1_000_000) + + +@pytest.mark.timeout(4) +def test_oom(): + glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 32767, 32767, 0, 0, 32767, 32767) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (1, 1))) + font.getmask("A" * 1_000_000) From 6cad0d62e7020e80fe706f7a239a2ec588f6f1b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:14:45 +1100 Subject: [PATCH 085/186] Do not crop again if glyph is the same as the previous one --- src/_imaging.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index e06780c75..8b5cac180 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2733,7 +2733,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) { static PyObject * _font_getmask(ImagingFontObject *self, PyObject *args) { Imaging im; - Imaging bitmap; + Imaging bitmap = NULL; int x, b; int i = 0; int status; @@ -2765,10 +2765,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { b = self->baseline; for (x = 0; text[i]; i++) { glyph = &self->glyphs[text[i]]; - bitmap = - ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); - if (!bitmap) { - goto failed; + if (i == 0 || text[i] != text[i - 1]) { + ImagingDelete(bitmap); + bitmap = + ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); + if (!bitmap) { + goto failed; + } } status = ImagingPaste( im, @@ -2778,17 +2781,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { glyph->dy0 + b, glyph->dx1 + x, glyph->dy1 + b); - ImagingDelete(bitmap); if (status < 0) { goto failed; } x = x + glyph->dx; b = b + glyph->dy; } + ImagingDelete(bitmap); free(text); return PyImagingNew(im); failed: + ImagingDelete(bitmap); free(text); ImagingDelete(im); Py_RETURN_NONE; From 492e5b0e0aa36e1d2d9a0d71280a9e1449b261dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 31 Dec 2023 16:58:03 +1100 Subject: [PATCH 086/186] Do not set default value for unused variable --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 8b5cac180..e0e5f804a 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2742,7 +2742,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { PyObject *encoded_string; unsigned char *text; - char *mode = ""; + char *mode; if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) { return NULL; From 1c183827e435b32cd192e6308a5057dbcfb56cea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 16:47:37 +1100 Subject: [PATCH 087/186] Added release notes for #7589 --- docs/releasenotes/10.2.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 0244b6f77..ca5750734 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -96,6 +96,12 @@ Support has been added to read the BC4U format of DDS images. Support has also been added to read DX10 BC1 and BC4, whether UNORM or TYPELESS. +Support arbitrary masks for uncompressed RGB DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All masks are now supported when reading DDS images with uncompressed RGB data, +allowing for bit counts other than 24 and 32. + Optimized ImageStat.Stat count and extrema ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From f6aa00dd1eea939a1e0841d4cc7fb7a9e0909de5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 17:00:50 +1100 Subject: [PATCH 088/186] Added release notes for #7654 --- docs/releasenotes/10.2.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index ca5750734..4ed8df98a 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -102,6 +102,12 @@ Support arbitrary masks for uncompressed RGB DDS images All masks are now supported when reading DDS images with uncompressed RGB data, allowing for bit counts other than 24 and 32. +Saving TIFF tag RowsPerStrip +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving TIFF images, the TIFF tag RowsPerStrip can now be one of the tags set by +the user, rather than always being calculated by Pillow. + Optimized ImageStat.Stat count and extrema ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 2aa08a59eaca9bf839909e5a6760992d27602fe9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 18:20:25 +1100 Subject: [PATCH 089/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6062ad742..3c8b4d115 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Trim glyph size in ImageFont.getmask() #7669 + [radarhere] + - Deprecate IptcImagePlugin helpers #7664 [nulano, hugovk, radarhere] From 681f8183f3e7b0755f345e8cbf12237af51d5492 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 1 Jan 2024 10:01:42 +0200 Subject: [PATCH 090/186] Remove member-order Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/PIL.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/PIL.rst b/docs/PIL.rst index 493d0bde3..b617d9205 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -74,7 +74,6 @@ can be found here. .. automodule:: PIL.ImageMode :members: - :member-order: bysource :undoc-members: :show-inheritance: From c08426b8343d1b84e02068df877e24e294cc9cbd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 1 Jan 2024 10:02:22 +0200 Subject: [PATCH 091/186] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/PIL.rst | 2 +- docs/releasenotes/10.2.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PIL.rst b/docs/PIL.rst index b617d9205..b6944e234 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -70,7 +70,7 @@ can be found here. :show-inheritance: :mod:`~PIL.ImageMode` Module ------------------------------ +---------------------------- .. automodule:: PIL.ImageMode :members: diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 46cfe1599..75ed461ba 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -114,7 +114,7 @@ Type hints Work has begun to add type annotations to Pillow, including: * :py:mod:`~PIL.ContainerIO` -* :py:mod:`~PIL.FontFile` and subclasses +* :py:mod:`~PIL.FontFile`, :py:mod:`~PIL.BdfFontFile` and :py:mod:`~PIL.PcfFontFile` * :py:mod:`~PIL.ImageChops` * :py:mod:`~PIL.ImageMode` * :py:mod:`~PIL.ImageSequence` From 4da1e490368109658cbd1c367cad016b59a7293c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 19:22:01 +1100 Subject: [PATCH 092/186] Added type hints --- src/PIL/Image.py | 2 +- src/PIL/ImageTransform.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 055dbda7a..065b22dc9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2643,7 +2643,7 @@ class Image: resample=Resampling.NEAREST, fill=1, fillcolor=None, - ): + ) -> Image: """ Transforms this image. This method creates a new image with the given size, and the same mode as the original, and copies data diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 1fdaa9140..1dc3e210e 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -14,17 +14,26 @@ # from __future__ import annotations +from typing import Sequence + from . import Image class Transform(Image.ImageTransformHandler): - def __init__(self, data): + method: int + + def __init__(self, data: Sequence[int]) -> None: self.data = data - def getdata(self): + def getdata(self) -> tuple[int, Sequence[int]]: return self.method, self.data - def transform(self, size, image, **options): + def transform( + self, + size: tuple[int, int], + image: Image.Image, + **options: dict[str, str | int | tuple[int, ...] | list[int]], + ) -> Image.Image: # can be overridden method, data = self.getdata() return image.transform(size, method, data, **options) From 59100652b6e9d4eeb82c34d446fea0a1736a5942 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 20:33:34 +1100 Subject: [PATCH 093/186] Added type hints to ImageTransform --- docs/releasenotes/10.2.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index f8ef4b8c8..d1f549e9c 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -149,4 +149,5 @@ Work has begun to add type annotations to Pillow, including: * :py:mod:`~PIL.ImageChops` * :py:mod:`~PIL.ImageMode` * :py:mod:`~PIL.ImageSequence` +* :py:mod:`~PIL.ImageTransform` * :py:mod:`~PIL.TarIO` From 09ea12107940d6831c300db3bd6ed29a0888c5b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:09:01 +1100 Subject: [PATCH 094/186] Use enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/PIL/ImageTransform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 1dc3e210e..84c81f184 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -20,7 +20,7 @@ from . import Image class Transform(Image.ImageTransformHandler): - method: int + method: Image.Transform def __init__(self, data: Sequence[int]) -> None: self.data = data From 0eb661b88971b75738cb7fe03b77e57158188e70 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 11:35:42 +0100 Subject: [PATCH 095/186] do not crop ImageFont glyphs from negative coordinates --- Tests/test_imagefontpil.py | 4 +++- src/_imaging.c | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index fd07ee23b..9e0855101 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -66,7 +66,9 @@ def test_decompression_bomb(): @pytest.mark.timeout(4) def test_oom(): - glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 32767, 32767, 0, 0, 32767, 32767) + glyph = struct.pack( + ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 + ) fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) font = ImageFont.ImageFont() diff --git a/src/_imaging.c b/src/_imaging.c index e0e5f804a..59f80a354 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2652,6 +2652,14 @@ _font_new(PyObject *self_, PyObject *args) { // Do not allow glyphs to extend beyond bitmap image // Helps prevent DOS by stopping cropped images being larger than the original + if (self->glyphs[i].sx0 < 0) { + self->glyphs[i].dx0 -= self->glyphs[i].sx0; + self->glyphs[i].sx0 = 0; + } + if (self->glyphs[i].sy0 < 0) { + self->glyphs[i].dy0 -= self->glyphs[i].sy0; + self->glyphs[i].sy0 = 0; + } if (self->glyphs[i].sx1 > self->bitmap->xsize) { self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize; self->glyphs[i].sx1 = self->bitmap->xsize; From aed764fe8404926472499208a39e5bf90d861b2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 23:39:04 +1100 Subject: [PATCH 096/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3c8b4d115..006b6d567 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,8 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- -- Trim glyph size in ImageFont.getmask() #7669 - [radarhere] +- Trim glyph size in ImageFont.getmask() #7669, #7672 + [radarhere, nulano] - Deprecate IptcImagePlugin helpers #7664 [nulano, hugovk, radarhere] From 7c526a6c6bdc7cb947f0aee1d1ee17c266ff6c61 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Jan 2024 23:59:13 +1100 Subject: [PATCH 097/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 006b6d567..1e872d172 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553 + [bgilbert, radarhere] + - Trim glyph size in ImageFont.getmask() #7669, #7672 [radarhere, nulano] From de62b25ed318f1604aa4ccd6f942a04c6b2c8b59 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 18:06:46 +0100 Subject: [PATCH 098/186] fix image url in "Reading from URL" example --- docs/handbook/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index d79f2465f..523e2ad74 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -542,7 +542,7 @@ Reading from URL from PIL import Image from urllib.request import urlopen - url = "https://python-pillow.org/images/pillow-logo.png" + url = "https://python-pillow.org/assets/images/pillow-logo.png" img = Image.open(urlopen(url)) From cb41b0cc78eeefbd9ed2ce8c10f8d6d4c405a706 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 17:16:45 +0000 Subject: [PATCH 099/186] [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.1.7 → v0.1.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.7...v0.1.9) - [github.com/psf/black-pre-commit-mirror: 23.12.0 → 23.12.1](https://github.com/psf/black-pre-commit-mirror/compare/23.12.0...23.12.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d1c4b8015..6adc75b49 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.7 + rev: v0.1.9 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black From 90991428fa4ca39076efa6329792f30b962d7d49 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 20:05:16 +0100 Subject: [PATCH 100/186] add LCMS2 flags to ImageCms --- Tests/test_imagecms.py | 10 +++++++ src/PIL/ImageCms.py | 63 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 0dde82bd7..fec482f43 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -90,6 +90,16 @@ def test_sanity(): hopper().point(t) +def test_flags(): + assert ImageCms.Flags.NONE == 0 + assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE + assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE + + assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16) + assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255) + assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) + + def test_name(): skip_missing() # get profile information for file diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 643fce830..eafafd583 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -16,8 +16,10 @@ # below for the original description. from __future__ import annotations +import operator import sys -from enum import IntEnum +from enum import IntEnum, IntFlag +from functools import reduce from . import Image @@ -119,6 +121,48 @@ class Direction(IntEnum): # # flags + +class Flags(IntFlag): + # These are taken from lcms2.h (including comments) + NONE = 0 + NOCACHE = 0x0040 # Inhibit 1-pixel cache + NOOPTIMIZE = 0x0100 # Inhibit optimizations + NULLTRANSFORM = 0x0200 # Don't transform anyway + GAMUTCHECK = 0x1000 # Out of Gamut alarm + SOFTPROOFING = 0x4000 # Do softproofing + BLACKPOINTCOMPENSATION = 0x2000 + NOWHITEONWHITEFIXUP = 0x0004 # Don't fix scum dot + HIGHRESPRECALC = 0x0400 # Use more memory to give better accuracy + LOWRESPRECALC = 0x0800 # Use less memory to minimize resources + # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: + USE_8BITS_DEVICELINK = 0x0008 # Create 8 bits devicelinks + GUESSDEVICECLASS = 0x0020 # Guess device class (for transform2devicelink) + KEEP_SEQUENCE = 0x0080 # Keep profile sequence for devicelink creation + FORCE_CLUT = 0x0002 # Force CLUT optimization + CLUT_POST_LINEARIZATION = 0x0001 # create postlinearization tables if possible + CLUT_PRE_LINEARIZATION = 0x0010 # create prelinearization tables if possible + NONEGATIVES = 0x8000 # Prevent negative numbers in floating point transforms + COPY_ALPHA = 0x04000000 # Alpha channels are copied on cmsDoTransform() + NODEFAULTRESOURCEDEF = 0x01000000 + + _GRIDPOINTS_1 = 1 << 16 + _GRIDPOINTS_2 = 2 << 16 + _GRIDPOINTS_4 = 4 << 16 + _GRIDPOINTS_8 = 8 << 16 + _GRIDPOINTS_16 = 16 << 16 + _GRIDPOINTS_32 = 32 << 16 + _GRIDPOINTS_64 = 64 << 16 + _GRIDPOINTS_128 = 128 << 16 + + @staticmethod + def GRIDPOINTS(n: int) -> Flags: + # Fine-tune control over number of gridpoints + return Flags.NONE | ((n & 0xFF) << 16) + + +_MAX_FLAG = reduce(operator.or_, Flags) + + FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, @@ -142,11 +186,6 @@ FLAGS = { "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints } -_MAX_FLAG = 0 -for flag in FLAGS.values(): - if isinstance(flag, int): - _MAX_FLAG = _MAX_FLAG | flag - # --------------------------------------------------------------------. # Experimental PIL-level API @@ -218,7 +257,7 @@ class ImageCmsTransform(Image.ImagePointHandler): intent=Intent.PERCEPTUAL, proof=None, proof_intent=Intent.ABSOLUTE_COLORIMETRIC, - flags=0, + flags=Flags.NONE, ): if proof is None: self.transform = core.buildTransform( @@ -303,7 +342,7 @@ def profileToProfile( renderingIntent=Intent.PERCEPTUAL, outputMode=None, inPlace=False, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Applies an ICC transformation to a given image, mapping from @@ -420,7 +459,7 @@ def buildTransform( inMode, outMode, renderingIntent=Intent.PERCEPTUAL, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -482,7 +521,7 @@ def buildTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: @@ -505,7 +544,7 @@ def buildProofTransform( outMode, renderingIntent=Intent.PERCEPTUAL, proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC, - flags=FLAGS["SOFTPROOFING"], + flags=Flags.SOFTPROOFING, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -586,7 +625,7 @@ def buildProofTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: From 26b2aa5165c62ee38664e291206f8ff28a30bb39 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 02:07:59 +0100 Subject: [PATCH 101/186] document ImageCms.{ImageCmsProfile,Intent,Direction}; fix ImageCms.core.CmsProfile references (cherry picked from commit f2b1bbcf65b327c14646d4113302e3df59555110) --- docs/deprecations.rst | 4 ++-- docs/reference/ImageCms.rst | 21 ++++++++++++++++++++- docs/releasenotes/8.0.0.rst | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a42dc555f..0f9c75756 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -338,8 +338,8 @@ ImageCms.CmsProfile attributes .. deprecated:: 3.2.0 .. versionremoved:: 8.0.0 -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0, -they issued a :py:exc:`DeprecationWarning`: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed. +From 6.0.0, they issued a :py:exc:`DeprecationWarning`: ======================== =================================================== Removed Use instead diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 9b9b5e7b2..9b6be40b1 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -8,9 +8,26 @@ The :py:mod:`~PIL.ImageCms` module provides color profile management support using the LittleCMS2 color management engine, based on Kevin Cazabon's PyCMS library. +.. autoclass:: ImageCmsProfile + :members: + :special-members: __init__ .. autoclass:: ImageCmsTransform + :members: + :undoc-members: .. autoexception:: PyCMSError +Constants +--------- + +.. autoclass:: Intent + :members: + :member-order: bysource + :undoc-members: +.. autoclass:: Direction + :members: + :member-order: bysource + :undoc-members: + Functions --------- @@ -37,13 +54,15 @@ CmsProfile ---------- The ICC color profiles are wrapped in an instance of the class -:py:class:`CmsProfile`. The specification ICC.1:2010 contains more +:py:class:`~core.CmsProfile`. The specification ICC.1:2010 contains more information about the meaning of the values in ICC profiles. For convenience, all XYZ-values are also given as xyY-values (so they can be easily displayed in a chromaticity diagram, for example). +.. py:currentmodule:: PIL.ImageCms.core .. py:class:: CmsProfile + :canonical: PIL._imagingcms.CmsProfile .. py:attribute:: creation_date :type: Optional[datetime.datetime] diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 2bf299dd3..1fc245c9a 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -30,7 +30,7 @@ Image.fromstring, im.fromstring and im.tostring ImageCms.CmsProfile attributes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed: ======================== =================================================== Removed Use instead From 0b2e2b224fe430c74995664c0d8eec9df789727e Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 21:38:29 +0100 Subject: [PATCH 102/186] document ImageCms.Flags --- docs/reference/ImageCms.rst | 4 +++ src/PIL/ImageCms.py | 57 +++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 9b6be40b1..4ef5ac774 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -27,6 +27,10 @@ Constants :members: :member-order: bysource :undoc-members: +.. autoclass:: Flags + :members: + :member-order: bysource + :undoc-members: Functions --------- diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index eafafd583..9a7afe81f 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -123,26 +123,43 @@ class Direction(IntEnum): class Flags(IntFlag): - # These are taken from lcms2.h (including comments) + """Flags and documentation are taken from ``lcms2.h``.""" + NONE = 0 - NOCACHE = 0x0040 # Inhibit 1-pixel cache - NOOPTIMIZE = 0x0100 # Inhibit optimizations - NULLTRANSFORM = 0x0200 # Don't transform anyway - GAMUTCHECK = 0x1000 # Out of Gamut alarm - SOFTPROOFING = 0x4000 # Do softproofing + NOCACHE = 0x0040 + """Inhibit 1-pixel cache""" + NOOPTIMIZE = 0x0100 + """Inhibit optimizations""" + NULLTRANSFORM = 0x0200 + """Don't transform anyway""" + GAMUTCHECK = 0x1000 + """Out of Gamut alarm""" + SOFTPROOFING = 0x4000 + """Do softproofing""" BLACKPOINTCOMPENSATION = 0x2000 - NOWHITEONWHITEFIXUP = 0x0004 # Don't fix scum dot - HIGHRESPRECALC = 0x0400 # Use more memory to give better accuracy - LOWRESPRECALC = 0x0800 # Use less memory to minimize resources + NOWHITEONWHITEFIXUP = 0x0004 + """Don't fix scum dot""" + HIGHRESPRECALC = 0x0400 + """Use more memory to give better accuracy""" + LOWRESPRECALC = 0x0800 + """Use less memory to minimize resources""" # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: - USE_8BITS_DEVICELINK = 0x0008 # Create 8 bits devicelinks - GUESSDEVICECLASS = 0x0020 # Guess device class (for transform2devicelink) - KEEP_SEQUENCE = 0x0080 # Keep profile sequence for devicelink creation - FORCE_CLUT = 0x0002 # Force CLUT optimization - CLUT_POST_LINEARIZATION = 0x0001 # create postlinearization tables if possible - CLUT_PRE_LINEARIZATION = 0x0010 # create prelinearization tables if possible - NONEGATIVES = 0x8000 # Prevent negative numbers in floating point transforms - COPY_ALPHA = 0x04000000 # Alpha channels are copied on cmsDoTransform() + USE_8BITS_DEVICELINK = 0x0008 + """Create 8 bits devicelinks""" + GUESSDEVICECLASS = 0x0020 + """Guess device class (for transform2devicelink)""" + KEEP_SEQUENCE = 0x0080 + """Keep profile sequence for devicelink creation""" + FORCE_CLUT = 0x0002 + """Force CLUT optimization""" + CLUT_POST_LINEARIZATION = 0x0001 + """create postlinearization tables if possible""" + CLUT_PRE_LINEARIZATION = 0x0010 + """create prelinearization tables if possible""" + NONEGATIVES = 0x8000 + """Prevent negative numbers in floating point transforms""" + COPY_ALPHA = 0x04000000 + """Alpha channels are copied on cmsDoTransform()""" NODEFAULTRESOURCEDEF = 0x01000000 _GRIDPOINTS_1 = 1 << 16 @@ -156,7 +173,11 @@ class Flags(IntFlag): @staticmethod def GRIDPOINTS(n: int) -> Flags: - # Fine-tune control over number of gridpoints + """ + Fine-tune control over number of gridpoints + + :param n: :py:class:`int` in range ``0 <= n <= 255`` + """ return Flags.NONE | ((n & 0xFF) << 16) From 6956d0b2853f5c7ec5f6ec4c60725c5a7ee73aeb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 18:32:31 +1100 Subject: [PATCH 103/186] 10.2.0 version bump --- CHANGES.rst | 2 +- src/PIL/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1e872d172..85036f642 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,7 @@ Changelog (Pillow) ================== -10.2.0 (unreleased) +10.2.0 (2024-01-02) ------------------- - Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553 diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 7d994caf4..1018b96b5 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "10.2.0.dev0" +__version__ = "10.2.0" From fd148246499bfd5c7d55f871e3f1f6e608a9bc3f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 19:15:13 +1100 Subject: [PATCH 104/186] bbox on macOS is not 2x on retina screens --- docs/reference/ImageGrab.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 0b94032d5..5c365132f 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -22,7 +22,10 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. - Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used. + On macOS, this is not increased to 2x for retina screens, so the full + width of a retina screen would be 1440, not 2880. + On Windows OS, the top-left point may be negative if + ``all_screens=True`` is used. :param include_layered_windows: Includes layered windows. Windows OS only. .. versionadded:: 6.1.0 From dacd92853094414ab6a4326e7f5805666b92996c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 20:37:59 +1100 Subject: [PATCH 105/186] 10.3.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 1018b96b5..0568943b5 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "10.2.0" +__version__ = "10.3.0.dev0" From ec6a57f69d45c0444d7e38df57af6761524463fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 21:27:12 +1100 Subject: [PATCH 106/186] Updated description Co-authored-by: Hugo van Kemenade --- docs/reference/ImageGrab.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 5c365132f..e0e8d5a2f 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -22,10 +22,10 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. - On macOS, this is not increased to 2x for retina screens, so the full - width of a retina screen would be 1440, not 2880. - On Windows OS, the top-left point may be negative if - ``all_screens=True`` is used. + On macOS, this is not increased to 2x for Retina screens, so the full + width of a Retina screen would be 1440, not 2880. + On Windows, the top-left point may be negative if ``all_screens=True`` + is used. :param include_layered_windows: Includes layered windows. Windows OS only. .. versionadded:: 6.1.0 From 75015e9859183e426012aa59309473c479b59a59 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 22:43:56 +1100 Subject: [PATCH 107/186] Skip PyPy3.8 Windows wheel --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 060fc497e..5adff7ec1 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -143,6 +143,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" + CIBW_SKIP: pp38-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow From 5ddcf4d11493524627dbf4b65ad0ed76b7625168 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 Jan 2024 20:33:48 +1100 Subject: [PATCH 108/186] Package name is now lowercase in wheel filenames --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index b3fd72a52..97f4f8dcd 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -87,7 +87,7 @@ Released as needed privately to individual vendors for critical security-related and copy into `dist`. Check and upload them e.g.: ```bash python3 -m twine check --strict dist/* - python3 -m twine upload dist/Pillow-5.2.0* + python3 -m twine upload dist/pillow-5.2.0* ``` ## Publicize Release From 81ea98e4941af0940b4725a7cc187c689a3726e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Tue, 2 Jan 2024 14:10:07 +0100 Subject: [PATCH 109/186] document ImageCmsTransform's base class Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/reference/ImageCms.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 4ef5ac774..22ed516ce 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -14,6 +14,7 @@ Cazabon's PyCMS library. .. autoclass:: ImageCmsTransform :members: :undoc-members: + :show-inheritance: .. autoexception:: PyCMSError Constants From fc7088a561554aec8b6a4cf6517fdcc11554cb6b Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 14:52:12 +0100 Subject: [PATCH 110/186] improve ImageTransform documentation --- docs/PIL.rst | 8 ------- docs/reference/ImageTransform.rst | 35 +++++++++++++++++++++++++++++++ docs/reference/index.rst | 1 + src/PIL/Image.py | 4 ++++ src/PIL/ImageTransform.py | 13 +++++++----- 5 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 docs/reference/ImageTransform.rst diff --git a/docs/PIL.rst b/docs/PIL.rst index b6944e234..bdbf1373d 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -77,14 +77,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageTransform` Module ---------------------------------- - -.. automodule:: PIL.ImageTransform - :members: - :undoc-members: - :show-inheritance: - :mod:`~PIL.PaletteFile` Module ------------------------------ diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst new file mode 100644 index 000000000..127880182 --- /dev/null +++ b/docs/reference/ImageTransform.rst @@ -0,0 +1,35 @@ + +.. py:module:: PIL.ImageTransform +.. py:currentmodule:: PIL.ImageTransform + +:py:mod:`~PIL.ImageTransform` Module +==================================== + +The :py:mod:`~PIL.ImageTransform` module contains implementations of +:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin +:py:class:`.Image.Transform` methods. + +.. autoclass:: Transform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: AffineTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: ExtentTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: QuadTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: MeshTransform + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 5d6affa94..82c75e373 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -25,6 +25,7 @@ Reference ImageShow ImageStat ImageTk + ImageTransform ImageWin ExifTags TiffTags diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1bba9aad2..c56da5458 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2666,6 +2666,10 @@ class Image: def transform(self, size, data, resample, fill=1): # Return result + Implementations of :py:class:`~PIL.Image.ImageTransformHandler` + for some of the :py:class:`Transform` methods are provided + in :py:mod:`~PIL.ImageTransform`. + It may also be an object with a ``method.getdata`` method that returns a tuple supplying new ``method`` and ``data`` values:: diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 84c81f184..4f79500e6 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -20,12 +20,14 @@ from . import Image class Transform(Image.ImageTransformHandler): + """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" + method: Image.Transform def __init__(self, data: Sequence[int]) -> None: self.data = data - def getdata(self) -> tuple[int, Sequence[int]]: + def getdata(self) -> tuple[Image.Transform, Sequence[int]]: return self.method, self.data def transform( @@ -34,6 +36,7 @@ class Transform(Image.ImageTransformHandler): image: Image.Image, **options: dict[str, str | int | tuple[int, ...] | list[int]], ) -> Image.Image: + """Perform the transform. Called from :py:meth:`.Image.transform`.""" # can be overridden method, data = self.getdata() return image.transform(size, method, data, **options) @@ -51,7 +54,7 @@ class AffineTransform(Transform): This function can be used to scale, translate, rotate, and shear the original image. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows from an affine transform matrix. @@ -73,7 +76,7 @@ class ExtentTransform(Transform): rectangle in the current image. It is slightly slower than crop, but about as fast as a corresponding resize operation. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the input image's coordinate system. See :ref:`coordinate-system`. @@ -89,7 +92,7 @@ class QuadTransform(Transform): Maps a quadrilateral (a region defined by four corners) from the image to a rectangle of the given size. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the upper left, lower left, lower right, and upper right corner of the @@ -104,7 +107,7 @@ class MeshTransform(Transform): Define a mesh image transform. A mesh transform consists of one or more individual quad transforms. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param data: A list of (bbox, quad) tuples. """ From b4a82712887e14b7ff1fc6302fdd9b1a48ac2280 Mon Sep 17 00:00:00 2001 From: Nulano Date: Tue, 2 Jan 2024 17:26:11 +0100 Subject: [PATCH 111/186] update Windows 11 tested versions --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index fbcfbb907..4c58b4ebb 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -581,9 +581,9 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+----------------------------+------------------+--------------+ | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm64 | +| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 | +----------------------------------+----------------------------+------------------+--------------+ -| Windows 11 Pro | 3.11, 3.12 | 10.1.0 |x86-64 | +| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ | Windows 10 | 3.7 | 7.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ From d134110ace31194f5fab4a9c3a62563a5d2a91bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jan 2024 09:01:35 +1100 Subject: [PATCH 112/186] If bbox is omitted, screenshot is taken at 2x on Retina screens --- docs/reference/ImageGrab.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index e0e8d5a2f..db2987eb0 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -11,9 +11,9 @@ or the clipboard to a PIL image memory. .. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) - Take a snapshot of the screen. The pixels inside the bounding box are - returned as an "RGBA" on macOS, or an "RGB" image otherwise. - If the bounding box is omitted, the entire screen is copied. + Take a snapshot of the screen. The pixels inside the bounding box are returned as + an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, + the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is From 424737ef4906b5005f53b1642e5b538cadf391ed Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 Jan 2024 11:18:16 +1100 Subject: [PATCH 113/186] Updated macOS tested Pillow versions --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 4c58b4ebb..922720b9d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -510,7 +510,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm | +| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm | +----------------------------------+----------------------------+------------------+--------------+ | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | | +----------------------------+------------------+ | From 05e73702f2651897efbdc0ad39551f1a99b371d4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 10:51:10 +1100 Subject: [PATCH 114/186] Updated matrix variable name on Linux and macOS to match Windows --- .github/workflows/wheels.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5adff7ec1..85d9eba1c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -39,18 +39,18 @@ jobs: include: - name: "macOS x86_64" os: macos-latest - archs: x86_64 + cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" os: macos-latest - archs: arm64 + cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 - name: "manylinux_2_28 x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 build: "*manylinux*" manylinux: "manylinux_2_28" steps: @@ -67,7 +67,7 @@ jobs: python3 -m pip install -r .ci/requirements-cibw.txt python3 -m cibuildwheel --output-dir wheelhouse env: - CIBW_ARCHS: ${{ matrix.archs }} + CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} @@ -77,7 +77,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}-${{ matrix.archs }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl windows: From 85c552934af5ed378b034560ace23107bb4ec497 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 3 Jan 2024 17:45:41 +0200 Subject: [PATCH 115/186] Goodbye Travis CI --- .github/workflows/test-cygwin.yml | 2 -- .github/workflows/test-docker.yml | 2 -- .github/workflows/test-mingw.yml | 2 -- .github/workflows/test-windows.yml | 2 -- .github/workflows/test.yml | 2 -- .travis.yml | 52 ------------------------------ README.md | 3 -- RELEASING.md | 8 +---- docs/about.rst | 3 +- docs/index.rst | 4 --- 10 files changed, 2 insertions(+), 78 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 32ac6f65e..7244315ac 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index eb27b4bf7..3bb6856f6 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 115c2e9be..cdd51e2bb 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 86cd5b5fa..a0ef1c3f1 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -6,7 +6,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -14,7 +13,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7e112f43..05f78704b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8f8250809..000000000 --- a/.travis.yml +++ /dev/null @@ -1,52 +0,0 @@ -if: tag IS present OR type = api - -env: - global: - - CIBW_ARCHS=aarch64 - - CIBW_SKIP=pp38-* - -language: python -# Default Python version is usually 3.6 -python: "3.12" -dist: jammy -services: docker - -jobs: - include: - - name: "manylinux2014 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014 - - name: "manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28 - - name: "musllinux aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*musllinux*" - -install: - - python3 -m pip install -r .ci/requirements-cibw.txt - -script: - - python3 -m cibuildwheel --output-dir wheelhouse - - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/" - -# Upload wheels to GitHub Releases -deploy: - provider: releases - api_key: $GITHUB_RELEASE_TOKEN - file_glob: true - file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl" - on: - repo: python-pillow/Pillow - tags: true - skip_cleanup: true diff --git a/README.md b/README.md index e11bd2faa..6982676f5 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,6 @@ As of 2019, Pillow development is GitHub Actions build status (Wheels) - Travis CI wheels build status (aarch64) Code coverage diff --git a/RELEASING.md b/RELEASING.md index 97f4f8dcd..62f3627de 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Develop and prepare release in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. -* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them. +* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. @@ -83,12 +83,6 @@ Released as needed privately to individual vendors for critical security-related * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag. -* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) - and copy into `dist`. Check and upload them e.g.: - ```bash - python3 -m twine check --strict dist/* - python3 -m twine upload dist/pillow-5.2.0* - ``` ## Publicize Release diff --git a/docs/about.rst b/docs/about.rst index 872ac0ea6..da351ce2c 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,13 +6,12 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `GitHub Actions`_, `AppVeyor`_ and `Travis CI`_ +- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow -.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow .. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.org/project/Pillow/ diff --git a/docs/index.rst b/docs/index.rst index 4f577fe9c..053d55c3c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more Date: Tue, 2 Jan 2024 18:11:52 +0200 Subject: [PATCH 116/186] Build QEMU-emulated Linux aarch64 wheels on GitHub Actions --- .github/workflows/wheels.yml | 77 +++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 85d9eba1c..6ebf9a4c0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -30,7 +30,80 @@ env: FORCE_COLOR: 1 jobs: - build: + build-1-QEMU-emulated-wheels: + name: QEMU ${{ matrix.python-version }} ${{ matrix.spec }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - pp39 + - pp310 + - cp38 + - cp39 + - cp310 + - cp311 + - cp312 + spec: + - manylinux2014 + - manylinux_2_28 + - musllinux + exclude: + - { python-version: pp39, spec: musllinux } + - { python-version: pp310, spec: musllinux } + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Install cibuildwheel + run: | + python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels (manylinux) + if: matrix.spec != 'musllinux' + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS_LINUX: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" + # Extra options for manylinux. + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} + + - name: Build wheels (musllinux) + if: matrix.spec == 'musllinux' + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS_LINUX: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" + + - uses: actions/upload-artifact@v4 + with: + name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} + path: ./wheelhouse/*.whl + + build-2-native-wheels: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: @@ -187,7 +260,7 @@ jobs: pypi-publish: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [build, windows, sdist] + needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] runs-on: ubuntu-latest name: Upload release to PyPI environment: From 55944860a5dd4a38d786df035346b9d5ddf3aa1e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:50:06 +0200 Subject: [PATCH 117/186] Remove unused docker/setup-buildx-action --- .github/workflows/wheels.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6ebf9a4c0..4de599b81 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -65,10 +65,6 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt From 32ae1bd08a6e3d8b9d266a2e7c8b265ec3d5ad18 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:52:22 +0200 Subject: [PATCH 118/186] Use aarch64 instead of QEMU in job name --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4de599b81..d7a93c70c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -31,7 +31,7 @@ env: jobs: build-1-QEMU-emulated-wheels: - name: QEMU ${{ matrix.python-version }} ${{ matrix.spec }} + name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} runs-on: ubuntu-latest strategy: fail-fast: false From fd37d86accff55d366950b3f9d286c7174ebe9b4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 12:55:04 +0200 Subject: [PATCH 119/186] Skip non-wheel CI runs for tags --- .github/workflows/test-windows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 86cd5b5fa..94c2d4d70 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -2,6 +2,8 @@ name: Test Windows on: push: + branches: + - "**" paths-ignore: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" From bc3cf97649d76bc231ec99d41dfd6eb2be72b175 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 11:00:11 +1100 Subject: [PATCH 120/186] Use general arch setting instead of platform-specific setting --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d7a93c70c..8a9f81dfd 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -76,7 +76,7 @@ jobs: env: # Build only the currently selected Linux architecture (so we can # parallelise for speed). - CIBW_ARCHS_LINUX: "aarch64" + CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" # Extra options for manylinux. @@ -90,7 +90,7 @@ jobs: env: # Build only the currently selected Linux architecture (so we can # parallelise for speed). - CIBW_ARCHS_LINUX: "aarch64" + CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" From e84b0a401509325c7e5e553a9f1a4591256b4b45 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 11:38:41 +1100 Subject: [PATCH 121/186] Combine build steps --- .github/workflows/wheels.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8a9f81dfd..e2bc78b98 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -69,8 +69,7 @@ jobs: run: | python3 -m pip install -r .ci/requirements-cibw.txt - - name: Build wheels (manylinux) - if: matrix.spec != 'musllinux' + - name: Build wheels run: | python3 -m cibuildwheel --output-dir wheelhouse env: @@ -78,22 +77,11 @@ jobs: # parallelise for speed). CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-manylinux*" + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" # Extra options for manylinux. CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} - - name: Build wheels (musllinux) - if: matrix.spec == 'musllinux' - run: | - python3 -m cibuildwheel --output-dir wheelhouse - env: - # Build only the currently selected Linux architecture (so we can - # parallelise for speed). - CIBW_ARCHS: "aarch64" - # Likewise, select only one Python version per job to speed this up. - CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec }}*" - - uses: actions/upload-artifact@v4 with: name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} From 60e82e5a3f21b25e1b54ebc1104e972290201a85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Jan 2024 17:24:09 +1100 Subject: [PATCH 122/186] Separate cibuildwheel install --- .github/workflows/wheels.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e2bc78b98..50e47f198 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -119,9 +119,11 @@ jobs: with: python-version: "3.x" - - name: Build wheels + - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels python3 -m cibuildwheel --output-dir wheelhouse env: CIBW_ARCHS: ${{ matrix.cibw_arch }} @@ -163,6 +165,10 @@ jobs: with: python-version: "3.x" + - name: Install cibuildwheel + run: | + & python.exe -m pip install -r .ci/requirements-cibw.txt + - name: Prepare for build run: | choco install nasm --no-progress @@ -171,8 +177,6 @@ jobs: # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images - & python.exe -m pip install -r .ci/requirements-cibw.txt - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }} shell: pwsh From 46db79abe1b8ada6f980e01cee633f9c8f6fbbdf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 4 Jan 2024 00:30:46 -0700 Subject: [PATCH 123/186] Fix syntax --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 50e47f198..77c1489df 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -124,6 +124,7 @@ jobs: python3 -m pip install -r .ci/requirements-cibw.txt - name: Build wheels + run: | python3 -m cibuildwheel --output-dir wheelhouse env: CIBW_ARCHS: ${{ matrix.cibw_arch }} From f184775cd3a2410ad326572d2b1d9a75ac481790 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 4 Jan 2024 23:32:54 +1100 Subject: [PATCH 124/186] Removed leading ampersand Co-authored-by: Hugo van Kemenade --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 77c1489df..be8f65244 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -168,7 +168,7 @@ jobs: - name: Install cibuildwheel run: | - & python.exe -m pip install -r .ci/requirements-cibw.txt + python.exe -m pip install -r .ci/requirements-cibw.txt - name: Prepare for build run: | From 2dd00de1f36d0e5dfd15f28048c2e2b7550bb232 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 20:26:14 +0100 Subject: [PATCH 125/186] rename x64 to AMD64 in winbuild/build_prepare.py --- .appveyor.yml | 2 +- .github/workflows/wheels.yml | 17 +++++++---------- winbuild/build.rst | 6 +++--- winbuild/build_prepare.py | 4 ++-- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 0f5dea9c5..4c5a7f9ee 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,7 +14,7 @@ environment: ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python38-x64 - ARCHITECTURE: x64 + ARCHITECTURE: AMD64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 85d9eba1c..36e98aa55 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -81,18 +81,15 @@ jobs: path: ./wheelhouse/*.whl windows: - name: Windows ${{ matrix.arch }} + name: Windows ${{ matrix.cibw_arch }} runs-on: windows-latest strategy: fail-fast: false matrix: include: - - arch: x86 - cibw_arch: x86 - - arch: x64 - cibw_arch: AMD64 - - arch: ARM64 - cibw_arch: ARM64 + - cibw_arch: x86 + - cibw_arch: AMD64 + - cibw_arch: ARM64 steps: - uses: actions/checkout@v4 @@ -116,7 +113,7 @@ jobs: & python.exe -m pip install -r .ci/requirements-cibw.txt - & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }} + & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} shell: pwsh - name: Build wheels @@ -157,13 +154,13 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: dist-windows-${{ matrix.arch }} + name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - name: Upload fribidi.dll uses: actions/upload-artifact@v4 with: - name: fribidi-windows-${{ matrix.arch }} + name: fribidi-windows-${{ matrix.cibw_arch }} path: winbuild\build\bin\fribidi* sdist: diff --git a/winbuild/build.rst b/winbuild/build.rst index a8e4ebaa6..cd3b559e7 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -27,7 +27,7 @@ Download and install: * `Ninja `_ (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) -* x86/x64: `Netwide Assembler (NASM) `_ +* x86/AMD64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x86,x64,ARM64}] [--nmake] + [--architecture {x86,AMD64,ARM64}] [--nmake] [--no-imagequant] [--no-fribidi] Download and generate build scripts for Pillow dependencies. @@ -55,7 +55,7 @@ Run ``build_prepare.py`` to configure the build:: --depends PILLOW_DEPS directory used to store cached dependencies (default: 'winbuild\depends') - --architecture {x86,x64,ARM64} + --architecture {x86,AMD64,ARM64} build architecture (default: same as host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 8e3757ca8..440e64d98 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -105,7 +105,7 @@ SF_PROJECTS = "https://sourceforge.net/projects" ARCHITECTURES = { "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, - "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, + "AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } @@ -651,7 +651,7 @@ if __name__ == "__main__": ( "ARM64" if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64") + else ("x86" if struct.calcsize("P") == 4 else "AMD64") ), ), help="build architecture (default: same as host Python)", From 5e2ebaface37ff71e1b8e4e9b5708ff3eec3a909 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 21:00:06 +0100 Subject: [PATCH 126/186] winbuild: build libwebp using cmake --- winbuild/build_prepare.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 440e64d98..1615abbdb 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -175,22 +175,15 @@ DEPS = { "dir": "libwebp-1.3.2", "license": "COPYING", "build": [ - cmd_rmdir(r"output\release-static"), # clean - cmd_nmake( - "Makefile.vc", - "all", - [ - "CFG=release-static", - "RTLIBCFG=dynamic", - "OBJDIR=output", - "ARCH={architecture}", - "LIBWEBP_BASENAME=webp", - ], + *cmds_cmake( + "webp webpdemux webpmux", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWEBP_LINK_STATIC:BOOL=OFF", ), cmd_mkdir(r"{inc_dir}\webp"), cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), ], - "libs": [r"output\release-static\{architecture}\lib\*.lib"], + "libs": [r"libwebp*.lib"], }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", @@ -204,7 +197,7 @@ DEPS = { }, r"libtiff\tif_webp.c": { # link against webp.lib - "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 + "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 }, r"test\CMakeLists.txt": { "add_executable(test_write_read_tags ../placeholder.h)": "", @@ -217,6 +210,7 @@ DEPS = { *cmds_cmake( "tiff", "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', ) ], From 4094edd12f32a432309d279d7c7b90d068f3bdb1 Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 21:26:47 +0100 Subject: [PATCH 127/186] winbuild: fix libwebp linking libsharpyuv --- winbuild/build_prepare.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1615abbdb..84284ae10 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -174,6 +174,11 @@ DEPS = { "filename": "libwebp-1.3.2.tar.gz", "dir": "libwebp-1.3.2", "license": "COPYING", + "patch": { + r"src\enc\picture_csp_enc.c": { + '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 + } + }, "build": [ *cmds_cmake( "webp webpdemux webpmux", @@ -183,7 +188,7 @@ DEPS = { cmd_mkdir(r"{inc_dir}\webp"), cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), ], - "libs": [r"libwebp*.lib"], + "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", From eff9f06f0dac6521d89938aa5a93ab03a77fd50d Mon Sep 17 00:00:00 2001 From: Nulano Date: Thu, 4 Jan 2024 22:10:11 +0100 Subject: [PATCH 128/186] fix comments --- winbuild/build_prepare.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 84284ae10..df33ea493 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -176,6 +176,7 @@ DEPS = { "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { + # link against libsharpyuv.lib '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 } }, @@ -201,7 +202,7 @@ DEPS = { "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 }, r"libtiff\tif_webp.c": { - # link against webp.lib + # link against libwebp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 }, r"test\CMakeLists.txt": { From d329207e62125ee3dcda0b2a82112757e9ef6fbd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Jan 2024 07:03:40 +1100 Subject: [PATCH 129/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 85036f642..ac961a680 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +10.3.0 (unreleased) +------------------- + +- Rename x64 to AMD64 in winbuild #7693 + [nulano] + 10.2.0 (2024-01-02) ------------------- From 2d6ad5868dac031a8b4eeda41116b3da3fd1be58 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 6 Jan 2024 12:07:55 +1100 Subject: [PATCH 130/186] Use "non-zero" consistently --- Tests/test_file_eps.py | 4 ++-- Tests/test_image_resample.py | 2 +- src/libImaging/GifEncode.c | 2 +- src/libImaging/Jpeg.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index c479c384a..8b48e83ad 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -270,7 +270,7 @@ def test_render_scale1(): image1_scale1_compare.load() assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale1: image2_scale1.load() with Image.open(FILE2_COMPARE) as image2_scale1_compare: @@ -292,7 +292,7 @@ def test_render_scale2(): image1_scale2_compare.load() assert_image_similar(image1_scale2, image1_scale2_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale2: image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index b4bf6c8df..5a578dba5 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -403,7 +403,7 @@ class TestCoreResampleCoefficients: if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] - def test_nonzero_coefficients(self): + def test_non_zero_coefficients(self): # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index f23245405..e37301df7 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -105,7 +105,7 @@ encode_loop: st->head = st->codes[st->probe] >> 20; goto encode_loop; } else { - /* Reprobe decrement must be nonzero and relatively prime to table + /* Reprobe decrement must be non-zero and relatively prime to table * size. So, any odd positive number for power-of-2 size. */ if ((st->probe -= ((st->tail << 2) | 1)) < 0) { st->probe += TABLE_SIZE; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 98eaac28d..7cdba9022 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -74,7 +74,7 @@ typedef struct { /* Optimize Huffman tables (slow) */ int optimize; - /* Disable automatic conversion of RGB images to YCbCr if nonzero */ + /* Disable automatic conversion of RGB images to YCbCr if non-zero */ int keep_rgb; /* Stream type (0=full, 1=tables only, 2=image only) */ From 0d841aab9a51a0b9433bc5932dcd913c028301cf Mon Sep 17 00:00:00 2001 From: Nulano Date: Sat, 6 Jan 2024 13:37:43 +0100 Subject: [PATCH 131/186] add support for grayscale pfm image format --- Tests/images/hopper.pfm | Bin 0 -> 65552 bytes Tests/images/hopper_be.pfm | Bin 0 -> 65551 bytes Tests/test_file_ppm.py | 45 ++++++++++++++++++++++++++- docs/handbook/image-file-formats.rst | 18 +++++++++++ src/PIL/PpmImagePlugin.py | 26 +++++++++++++--- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 Tests/images/hopper.pfm create mode 100644 Tests/images/hopper_be.pfm diff --git a/Tests/images/hopper.pfm b/Tests/images/hopper.pfm new file mode 100644 index 0000000000000000000000000000000000000000..b576615640136f3229ab5a3d29ab7956fd194dd0 GIT binary patch literal 65552 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 L`NZ}X?0@thg2ah6 literal 0 HcmV?d00001 diff --git a/Tests/images/hopper_be.pfm b/Tests/images/hopper_be.pfm new file mode 100644 index 0000000000000000000000000000000000000000..93c75e26fda2cd69362a58f61373b0d88075e51c GIT binary patch literal 65551 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 Date: Sat, 6 Jan 2024 14:33:11 +0100 Subject: [PATCH 132/186] Add code formatting to ImageCms.Flags docstrings Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/ImageCms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 9a7afe81f..62b010f45 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -147,7 +147,7 @@ class Flags(IntFlag): USE_8BITS_DEVICELINK = 0x0008 """Create 8 bits devicelinks""" GUESSDEVICECLASS = 0x0020 - """Guess device class (for transform2devicelink)""" + """Guess device class (for ``transform2devicelink``)""" KEEP_SEQUENCE = 0x0080 """Keep profile sequence for devicelink creation""" FORCE_CLUT = 0x0002 @@ -159,7 +159,7 @@ class Flags(IntFlag): NONEGATIVES = 0x8000 """Prevent negative numbers in floating point transforms""" COPY_ALPHA = 0x04000000 - """Alpha channels are copied on cmsDoTransform()""" + """Alpha channels are copied on ``cmsDoTransform()``""" NODEFAULTRESOURCEDEF = 0x01000000 _GRIDPOINTS_1 = 1 << 16 From a786a0551b75ab3e85da1bfad54226faa2336022 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 16:17:57 +1100 Subject: [PATCH 133/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ac961a680..ebf731c56 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Add LCMS2 flags to ImageCms #7676 + [nulano, radarhere, hugovk] + - Rename x64 to AMD64 in winbuild #7693 [nulano] From bb5527484536f6c8e90ffffd95906c352fecca18 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 18:49:01 +1100 Subject: [PATCH 134/186] Removed PPM loop to read header tokens --- src/PIL/PpmImagePlugin.py | 49 ++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 25dbfa5b0..82314214a 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -105,40 +105,31 @@ class PpmImageFile(ImageFile.ImageFile): elif magic_number in (b"P3", b"P6"): self.custom_mimetype = "image/x-portable-pixmap" - maxval = None + self._size = int(self._read_token()), int(self._read_token()) + decoder_name = "raw" if magic_number in (b"P1", b"P2", b"P3"): decoder_name = "ppm_plain" - for ix in range(3): - token = int(self._read_token()) - if ix == 0: # token is the x size - xsize = token - elif ix == 1: # token is the y size - ysize = token - if mode == "1": - self._mode = "1" - rawmode = "1;I" - break - else: - self._mode = rawmode = mode - elif ix == 2: # token is maxval - maxval = token - if not 0 < maxval < 65536: - msg = "maxval must be greater than 0 and less than 65536" - raise ValueError(msg) - if maxval > 255 and mode == "L": - self._mode = "I" + if mode == "1": + self._mode = "1" + args = "1;I" + else: + maxval = int(self._read_token()) + if not 0 < maxval < 65536: + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) + self._mode = "I" if maxval > 255 and mode == "L" else mode - if decoder_name != "ppm_plain": - # If maxval matches a bit depth, use the raw decoder directly - if maxval == 65535 and mode == "L": - rawmode = "I;16B" - elif maxval != 255: - decoder_name = "ppm" + rawmode = mode + if decoder_name != "ppm_plain": + # If maxval matches a bit depth, use the raw decoder directly + if maxval == 65535 and mode == "L": + rawmode = "I;16B" + elif maxval != 255: + decoder_name = "ppm" - args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) - self._size = xsize, ysize - self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)] + args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) + self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] # From ba6399cad14d816cafc44c25782d6a3153082ae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Jan 2024 19:34:27 +1100 Subject: [PATCH 135/186] Added PerspectiveTransform --- Tests/test_image_transform.py | 2 ++ docs/reference/ImageTransform.rst | 5 +++++ src/PIL/ImageTransform.py | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 15939ef64..578a0a296 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -16,6 +16,8 @@ class TestImageTransform: transform = ImageTransform.AffineTransform(seq[:6]) im.transform((100, 100), transform) + transform = ImageTransform.PerspectiveTransform(seq[:8]) + im.transform((100, 100), transform) transform = ImageTransform.ExtentTransform(seq[:4]) im.transform((100, 100), transform) transform = ImageTransform.QuadTransform(seq[:8]) diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst index 127880182..5b0a5ce49 100644 --- a/docs/reference/ImageTransform.rst +++ b/docs/reference/ImageTransform.rst @@ -19,6 +19,11 @@ The :py:mod:`~PIL.ImageTransform` module contains implementations of :undoc-members: :show-inheritance: +.. autoclass:: PerspectiveTransform + :members: + :undoc-members: + :show-inheritance: + .. autoclass:: ExtentTransform :members: :undoc-members: diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 4f79500e6..6aa82dadd 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -63,6 +63,26 @@ class AffineTransform(Transform): method = Image.Transform.AFFINE +class PerspectiveTransform(Transform): + """ + Define a perspective image transform. + + This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel + (x, y) in the output image, the new value is taken from a position + ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in + the input image, rounded to nearest pixel. + + This function can be used to scale, translate, rotate, and shear the + original image. + + See :py:meth:`.Image.transform` + + :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). + """ + + method = Image.Transform.PERSPECTIVE + + class ExtentTransform(Transform): """ Define a transform to extract a subregion from an image. From 6d99f9193f0abe211e611f2d9909a4eb5881d270 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Sun, 7 Jan 2024 16:00:58 -0500 Subject: [PATCH 136/186] Fix info for first frame of apng images getting clobbered when seeking to the first frame multiple times. --- src/PIL/PngImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index e4ed93880..823f12492 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -378,7 +378,7 @@ class PngStream(ChunkStream): } def rewind(self): - self.im_info = self.rewind_state["info"] + self.im_info = self.rewind_state["info"].copy() self.im_tile = self.rewind_state["tile"] self._seq_num = self.rewind_state["seq_num"] From 08f11c57a131b402f405db76dfd411b441df1ab5 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 1 Jan 2024 20:52:47 +0100 Subject: [PATCH 137/186] deprecate ImageCms members: DESCRIPTION, VERSION, FLAGS, versions() --- Tests/test_imagecms.py | 13 +++++++++++-- src/PIL/ImageCms.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index fec482f43..9575b026d 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -49,8 +49,8 @@ def skip_missing(): def test_sanity(): # basic smoke test. # this mostly follows the cms_test outline. - - v = ImageCms.versions() # should return four strings + with pytest.warns(DeprecationWarning): + v = ImageCms.versions() # should return four strings assert v[0] == "1.0.0 pil" assert list(map(type, v)) == [str, str, str, str] @@ -637,3 +637,12 @@ def test_rgb_lab(mode): im = Image.new("LAB", (1, 1), (255, 0, 0)) converted_im = im.convert(mode) assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) + + +def test_deprecation(): + with pytest.warns(DeprecationWarning): + assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") + with pytest.warns(DeprecationWarning): + assert ImageCms.VERSION == "1.0.0 pil" + with pytest.warns(DeprecationWarning): + assert isinstance(ImageCms.FLAGS, dict) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 62b010f45..827755bf6 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -20,8 +20,10 @@ import operator import sys from enum import IntEnum, IntFlag from functools import reduce +from typing import Any from . import Image +from ._deprecate import deprecate try: from . import _imagingcms @@ -32,7 +34,7 @@ except ImportError as ex: _imagingcms = DeferredError.new(ex) -DESCRIPTION = """ +_DESCRIPTION = """ pyCMS a Python / PIL interface to the littleCMS ICC Color Management System @@ -95,7 +97,22 @@ pyCMS """ -VERSION = "1.0.0 pil" +_VERSION = "1.0.0 pil" + + +def __getattr__(name: str) -> Any: + if name == "DESCRIPTION": + deprecate("PIL.ImageCms.DESCRIPTION", 12) + return _DESCRIPTION + elif name == "VERSION": + deprecate("PIL.ImageCms.VERSION", 12) + return _VERSION + elif name == "FLAGS": + deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") + return _FLAGS + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + # --------------------------------------------------------------------. @@ -184,7 +201,7 @@ class Flags(IntFlag): _MAX_FLAG = reduce(operator.or_, Flags) -FLAGS = { +_FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, "MATRIXONLY": (1 | 2), @@ -1064,4 +1081,9 @@ def versions(): (pyCMS) Fetches versions. """ - return VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ + deprecate( + "PIL.ImageCms.versions()", + 12, + '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', + ) + return _VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ From ccdea48cf379fac585918001f304a7ab58448b96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Jan 2024 10:36:30 +1100 Subject: [PATCH 138/186] Added identity tests for Transform classes --- Tests/test_image_transform.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 578a0a296..f5d5ab704 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -10,20 +10,25 @@ from .helper import assert_image_equal, assert_image_similar, hopper class TestImageTransform: def test_sanity(self): - im = Image.new("L", (100, 100)) + im = hopper() - seq = tuple(range(10)) - - transform = ImageTransform.AffineTransform(seq[:6]) - im.transform((100, 100), transform) - transform = ImageTransform.PerspectiveTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.ExtentTransform(seq[:4]) - im.transform((100, 100), transform) - transform = ImageTransform.QuadTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) - im.transform((100, 100), transform) + for transform in ( + ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)), + ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)), + ImageTransform.ExtentTransform((0, 0) + im.size), + ImageTransform.QuadTransform( + (0, 0, 0, im.height, im.width, im.height, im.width, 0) + ), + ImageTransform.MeshTransform( + [ + ( + (0, 0) + im.size, + (0, 0, 0, im.height, im.width, im.height, im.width, 0), + ) + ] + ), + ): + assert_image_equal(im, im.transform(im.size, transform)) def test_info(self): comment = b"File written by Adobe Photoshop\xa8 4.0" From edc46e223b1275f9b197c33011f305af2975afe3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 01:27:41 +0100 Subject: [PATCH 139/186] document ImageCms deprecations --- docs/deprecations.rst | 39 ++++++++++++++++- docs/reference/ImageCms.rst | 3 ++ docs/releasenotes/10.3.0.rst | 83 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/9.1.0.rst | 2 +- docs/releasenotes/index.rst | 1 + 5 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 docs/releasenotes/10.3.0.rst diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0f9c75756..7602f8b4e 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -55,6 +55,43 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant for internal use, so there is no replacement. They can each be replaced by a single line of code using builtin functions in Python. +ImageCms constants and versions() function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.3.0 + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been +replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``ImageCms.DESCRIPTION`` +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +===================================================== ============================================================ + Removed features ---------------- @@ -118,7 +155,7 @@ Constants .. versionremoved:: 10.0.0 A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 22ed516ce..c4484cbe2 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -24,14 +24,17 @@ Constants :members: :member-order: bysource :undoc-members: + :show-inheritance: .. autoclass:: Direction :members: :member-order: bysource :undoc-members: + :show-inheritance: .. autoclass:: Flags :members: :member-order: bysource :undoc-members: + :show-inheritance: Functions --------- diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 000000000..ddcb38aa1 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,83 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been replaced +with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``ImageCms.DESCRIPTION`` +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +===================================================== ============================================================ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 02da702a7..6400218f4 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -51,7 +51,7 @@ Constants ^^^^^^^^^ A number of constants have been deprecated and will be removed in Pillow 10.0.0 -(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. +(2023-07-01). Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853c..e86f8082b 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.3.0 10.2.0 10.1.0 10.0.1 From bb855583ea3320b637e7f6511721a9e2655f2b99 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 01:55:32 +0100 Subject: [PATCH 140/186] Update PyPI links to use pillow (lowercase) --- README.md | 4 ++-- docs/about.rst | 2 +- docs/index.rst | 4 ++-- docs/installation.rst | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6982676f5..6ca870166 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,10 @@ As of 2019, Pillow development is Tidelift - Newest PyPI version - Number of PyPI downloads `_ and by direct URL access -eg. https://pypi.org/project/Pillow/1.0/. +`_ and by direct URL access +eg. https://pypi.org/project/pillow/1.0/. From 3515f997ce06664d8ec42cc21e39eef9897dbaeb Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Sun, 7 Jan 2024 20:42:52 -0500 Subject: [PATCH 141/186] Add test against info of apng images getting clobbered when seeking to the first frame multiple times. --- Tests/images/apng/issue_7700.png | Bin 0 -> 233 bytes Tests/test_file_apng.py | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 Tests/images/apng/issue_7700.png diff --git a/Tests/images/apng/issue_7700.png b/Tests/images/apng/issue_7700.png new file mode 100644 index 0000000000000000000000000000000000000000..984254b8e5632f9f1bf148485a77bb6a141dcb4b GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`9Er&xK0ulYh#5ZjPA&jaQfUw| zkPuh{hyel)U@p%GyA42&x~Gd{NX4Aw6SvYo%ojStp!80BRioiiwh7HElNnUodZY}p z4j33RFfd-v>sQUc#0b;^GZ@51GuQzr6w`P67?6@pOK}VV(o8_Z4dze!4m2ES)C$JM dY&_!34DTjb$p7CmBOj=M!PC{xWt~$(698zJHUj_v literal 0 HcmV?d00001 diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 60d951636..8069d4b08 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -689,3 +689,13 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat ) with Image.open(test_file) as reloaded: assert reloaded.mode == mode + + +def test_apng_issue_7700(): + # https://github.com/python-pillow/Pillow/issues/7700 + with Image.open("Tests/images/apng/issue_7700.png") as im: + for i in range(5): + im.seek(0) + assert im.info["duration"] == 4000.0 + im.seek(1) + assert im.info["duration"] == 1000.0 From bddfebc3315d85bf822192c57a33646d79e738c3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 12:57:23 +0100 Subject: [PATCH 142/186] add license comment to ImageCms; explicitly say "no replacement" for deprecations without a replacement --- docs/deprecations.rst | 4 ++-- docs/releasenotes/10.3.0.rst | 4 ++-- src/PIL/ImageCms.py | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 7602f8b4e..4c9abe195 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -67,11 +67,11 @@ replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``ImageCms.DESCRIPTION`` +``ImageCms.DESCRIPTION`` No replacement ``ImageCms.VERSION`` ``PIL.__version__`` ``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` ``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement ``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` ``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` ``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index ddcb38aa1..8dfe34d95 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -20,11 +20,11 @@ with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ -``ImageCms.DESCRIPTION`` +``ImageCms.DESCRIPTION`` No replacement ``ImageCms.VERSION`` ``PIL.__version__`` ``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` ``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement ``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` ``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` ``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 827755bf6..3e40105e4 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -4,6 +4,9 @@ # Optional color management support, based on Kevin Cazabon's PyCMS # library. +# Originally released under LGPL. Graciously donated to PIL in +# March 2009, for distribution under the standard PIL license + # History: # 2009-03-08 fl Added to PIL. From f044d53fd181446366ab78f570076a665933d4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Mon, 8 Jan 2024 17:17:17 +0100 Subject: [PATCH 143/186] swap conditions Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 6be1278eb..d43e21e14 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,7 @@ class PpmImageFile(ImageFile.ImageFile): for ix in range(3): if mode == "F" and ix == 2: scale = float(self._read_token()) - if not math.isfinite(scale) or scale == 0.0: + if scale == 0.0 or not math.isfinite(scale): msg = "scale must be finite and non-zero" raise ValueError(msg) rawmode = "F;32F" if scale < 0 else "F;32BF" From 5dd1652f2775bb916faa648fe48c7c6350d2c8a4 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 17:16:23 +0100 Subject: [PATCH 144/186] use filename instead of f --- Tests/test_file_ppm.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index efdb880de..d8e259b1c 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -89,20 +89,20 @@ def test_16bit_pgm(): def test_16bit_pgm_write(tmp_path): with Image.open("Tests/images/16_bit_binary.pgm") as im: - f = str(tmp_path / "temp.pgm") - im.save(f, "PPM") + filename = str(tmp_path / "temp.pgm") + im.save(filename, "PPM") - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pnm(tmp_path): with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) - f = str(tmp_path / "temp.pnm") - im.save(f) + filename = str(tmp_path / "temp.pnm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pfm(tmp_path): @@ -110,10 +110,10 @@ def test_pfm(tmp_path): assert im.info["scale"] == 1.0 assert_image_equal(im, hopper("F")) - f = str(tmp_path / "tmp.pfm") - im.save(f) + filename = str(tmp_path / "tmp.pfm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) def test_pfm_big_endian(tmp_path): @@ -121,10 +121,10 @@ def test_pfm_big_endian(tmp_path): assert im.info["scale"] == 2.5 assert_image_equal(im, hopper("F")) - f = str(tmp_path / "tmp.pfm") - im.save(f) + filename = str(tmp_path / "tmp.pfm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) @pytest.mark.parametrize( From 586e7740947933454589f3c60b22387e01fa5701 Mon Sep 17 00:00:00 2001 From: Nulano Date: Mon, 8 Jan 2024 17:35:01 +0100 Subject: [PATCH 145/186] add PFM support to release notes --- docs/handbook/image-file-formats.rst | 3 +- docs/releasenotes/10.3.0.rst | 49 ++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 docs/releasenotes/10.3.0.rst diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 02a6a3af7..569ccb769 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -701,7 +701,8 @@ PFM .. versionadded:: 10.3.0 -Pillow reads and writes grayscale (Pf format) PFM files containing ``F`` data. +Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files +containing ``F`` data. Color (PF format) PFM files are not supported. diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 000000000..34afbe4b8 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,49 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +Portable FloatMap (PFM) images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading and writing grayscale (Pf format) +Portable FloatMap (PFM) files containing ``F`` data. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853c..e86f8082b 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.3.0 10.2.0 10.1.0 10.0.1 From a844871c5eec479275af20ae0f06e4204174e9a6 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Mon, 8 Jan 2024 15:18:49 -0500 Subject: [PATCH 146/186] Give apng repeated seeks test and image a more descriptive name. --- ...700.png => repeated_seeks_give_correct_info.png} | Bin Tests/test_file_apng.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename Tests/images/apng/{issue_7700.png => repeated_seeks_give_correct_info.png} (100%) diff --git a/Tests/images/apng/issue_7700.png b/Tests/images/apng/repeated_seeks_give_correct_info.png similarity index 100% rename from Tests/images/apng/issue_7700.png rename to Tests/images/apng/repeated_seeks_give_correct_info.png diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 8069d4b08..47d425f8b 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -691,9 +691,9 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat assert reloaded.mode == mode -def test_apng_issue_7700(): +def test_apng_repeated_seeks_give_correct_info(): # https://github.com/python-pillow/Pillow/issues/7700 - with Image.open("Tests/images/apng/issue_7700.png") as im: + with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: for i in range(5): im.seek(0) assert im.info["duration"] == 4000.0 From a6051a4045354203d9fccafbe2ac620c505d9e00 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Mon, 8 Jan 2024 15:20:24 -0500 Subject: [PATCH 147/186] Add type hints and fix some formatting for the apng repeated seeks test. --- Tests/test_file_apng.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 47d425f8b..ea5ab41ec 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -691,11 +691,11 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat assert reloaded.mode == mode -def test_apng_repeated_seeks_give_correct_info(): +def test_apng_repeated_seeks_give_correct_info() -> None: # https://github.com/python-pillow/Pillow/issues/7700 with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: for i in range(5): im.seek(0) - assert im.info["duration"] == 4000.0 + assert im.info["duration"] == 4000 im.seek(1) - assert im.info["duration"] == 1000.0 + assert im.info["duration"] == 1000 From 931821688c0f9ef331097ac8b1358780eb82b21e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:22:25 +1100 Subject: [PATCH 148/186] Added release notes --- docs/releasenotes/10.3.0.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 34afbe4b8..391068769 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -26,10 +26,12 @@ TODO API Additions ============= -TODO -^^^^ +Added PerspectiveTransform +^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning +that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding +subclass of :py:class:`~PIL.ImageTransform.Transform`. Security ======== From 6c320323b44df48a5162a2113ae2c34e7c9bf564 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:47:27 +1100 Subject: [PATCH 149/186] Only set row order when needed --- src/PIL/PpmImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 83e028718..9d37dcde0 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -311,7 +311,6 @@ class PpmDecoder(ImageFile.PyDecoder): def _save(im, fp, filename): - row_order = 1 if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": @@ -322,7 +321,6 @@ def _save(im, fp, filename): rawmode, head = "RGB", b"P6" elif im.mode == "F": rawmode, head = "F;32F", b"Pf" - row_order = -1 else: msg = f"cannot write mode {im.mode} as PPM" raise OSError(msg) @@ -336,6 +334,7 @@ def _save(im, fp, filename): fp.write(b"65535\n") elif head == b"Pf": fp.write(b"-1.0\n") + row_order = -1 if im.mode == "F" else 1 ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) From ab262dbfd5b8076fd8d530e27c8e4896f03025e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 12:56:33 +1100 Subject: [PATCH 150/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ebf731c56..30bbaec3a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Add support for reading and writing grayscale PFM images #7696 + [nulano, hugovk] + - Add LCMS2 flags to ImageCms #7676 [nulano, radarhere, hugovk] From 1e8a03cd2d938d3773dfdbe9d477276e96527494 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 18:08:40 +1100 Subject: [PATCH 151/186] Link to Python enum documentation --- docs/reference/ExifTags.rst | 5 +++-- docs/releasenotes/10.0.0.rst | 2 +- docs/releasenotes/9.3.0.rst | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 464ab77ea..06965ead3 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -4,8 +4,9 @@ :py:mod:`~PIL.ExifTags` Module ============================== -The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes -which provide constants and clear-text names for various well-known EXIF tags. +The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` +classes which provide constants and clear-text names for various well-known +EXIF tags. .. py:data:: Base diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index a3f238119..705ca0415 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -43,7 +43,7 @@ Constants ^^^^^^^^^ A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. ===================================================== ============================================================ Removed Use instead diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index fde2faae3..16075ce95 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -33,8 +33,9 @@ Added ExifTags enums ^^^^^^^^^^^^^^^^^^^^ The data from :py:data:`~PIL.ExifTags.TAGS` and -:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum`` -classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`. +:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as +:py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and +:py:data:`~PIL.ExifTags.GPS`. Security From 71ba20bb19899f55761dae1ccd0bc662766cdc65 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 Jan 2024 18:22:10 +1100 Subject: [PATCH 152/186] Shortened table description --- docs/deprecations.rst | 54 ++++++++++++++++++------------------ docs/releasenotes/10.3.0.rst | 54 ++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4c9abe195..205fcb9ab 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -64,33 +64,33 @@ A number of constants and a function in :py:mod:`.ImageCms` have been deprecated This includes a table of flags based on LittleCMS version 1 which has been replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -===================================================== ============================================================ +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== Removed features ---------------- diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 8ce6f4b9c..548f95df9 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -17,33 +17,33 @@ A number of constants and a function in :py:mod:`.ImageCms` have been deprecated This includes a table of flags based on LittleCMS version 1 which has been replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -===================================================== ============================================================ +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== API Changes =========== From dc6d7611e9196447aaa18305c79ee83f005f4ec3 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Tue, 9 Jan 2024 08:55:49 -0500 Subject: [PATCH 153/186] Test apng repeated seeks 3 times instead of 5. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_apng.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index ea5ab41ec..340332258 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -692,9 +692,8 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat def test_apng_repeated_seeks_give_correct_info() -> None: - # https://github.com/python-pillow/Pillow/issues/7700 with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: - for i in range(5): + for i in range(3): im.seek(0) assert im.info["duration"] == 4000 im.seek(1) From d7874e8a03dbf57b01c0fe41290603ddfe2875c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 09:07:10 +1100 Subject: [PATCH 154/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 30bbaec3a..c267ca472 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Added PerspectiveTransform #7699 + [radarhere] + - Add support for reading and writing grayscale PFM images #7696 [nulano, hugovk] From 659098c6acc46ff353ba2a136805b463fb5cc97b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 22:05:26 +1100 Subject: [PATCH 155/186] Added type hints --- pyproject.toml | 1 - src/PIL/Image.py | 4 +- src/PIL/ImageMath.py | 136 +++++++++++++++++++++------------------ src/PIL/_imagingmath.pyi | 5 ++ 4 files changed, 80 insertions(+), 66 deletions(-) create mode 100644 src/PIL/_imagingmath.pyi diff --git a/pyproject.toml b/pyproject.toml index da2537b21..54a4bcaec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,6 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageMath.py$', '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', '^src/PIL/ImageShow.py$', diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c56da5458..0fbbe5861 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -873,7 +873,7 @@ class Image: def convert( self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256 - ): + ) -> Image: """ Returns a converted copy of this image. For the "P" mode, this method translates pixels through the palette. If mode is @@ -1305,7 +1305,7 @@ class Image: """ return ImageMode.getmode(self.mode).bands - def getbbox(self, *, alpha_only=True): + def getbbox(self, *, alpha_only=True) -> tuple[int, int, int, int]: """ Calculates the bounding box of the non-zero regions in the image. diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index b77f4bce5..949fa45bb 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,6 +17,7 @@ from __future__ import annotations import builtins +from typing import Any from . import Image, _imagingmath @@ -24,10 +25,10 @@ from . import Image, _imagingmath class _Operand: """Wraps an image operand, providing standard operators""" - def __init__(self, im): + def __init__(self, im: Image.Image): self.im = im - def __fixup(self, im1): + def __fixup(self, im1: _Operand | float) -> Image.Image: # convert image to suitable mode if isinstance(im1, _Operand): # argument was an image. @@ -45,122 +46,131 @@ class _Operand: else: return Image.new("F", self.im.size, im1) - def apply(self, op, im1, im2=None, mode=None): - im1 = self.__fixup(im1) + def apply( + self, + op: str, + im1: _Operand | float, + im2: _Operand | float | None = None, + mode: str | None = None, + ) -> _Operand: + im_1 = self.__fixup(im1) if im2 is None: # unary operation - out = Image.new(mode or im1.mode, im1.size, None) - im1.load() + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() try: - op = getattr(_imagingmath, op + "_" + im1.mode) + op = getattr(_imagingmath, op + "_" + im_1.mode) except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im1.im.id) + _imagingmath.unop(op, out.im.id, im_1.im.id) else: # binary operation - im2 = self.__fixup(im2) - if im1.mode != im2.mode: + im_2 = self.__fixup(im2) + if im_1.mode != im_2.mode: # convert both arguments to floating point - if im1.mode != "F": - im1 = im1.convert("F") - if im2.mode != "F": - im2 = im2.convert("F") - if im1.size != im2.size: + if im_1.mode != "F": + im_1 = im_1.convert("F") + if im_2.mode != "F": + im_2 = im_2.convert("F") + if im_1.size != im_2.size: # crop both arguments to a common size - size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1])) - if im1.size != size: - im1 = im1.crop((0, 0) + size) - if im2.size != size: - im2 = im2.crop((0, 0) + size) - out = Image.new(mode or im1.mode, im1.size, None) - im1.load() - im2.load() + size = ( + min(im_1.size[0], im_2.size[0]), + min(im_1.size[1], im_2.size[1]), + ) + if im_1.size != size: + im_1 = im_1.crop((0, 0) + size) + if im_2.size != size: + im_2 = im_2.crop((0, 0) + size) + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() + im_2.load() try: - op = getattr(_imagingmath, op + "_" + im1.mode) + op = getattr(_imagingmath, op + "_" + im_1.mode) except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) + _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) return _Operand(out) # unary operators - def __bool__(self): + def __bool__(self) -> bool: # an image is "true" if it contains at least one non-zero pixel return self.im.getbbox() is not None - def __abs__(self): + def __abs__(self) -> _Operand: return self.apply("abs", self) - def __pos__(self): + def __pos__(self) -> _Operand: return self - def __neg__(self): + def __neg__(self) -> _Operand: return self.apply("neg", self) # binary operators - def __add__(self, other): + def __add__(self, other: _Operand | float) -> _Operand: return self.apply("add", self, other) - def __radd__(self, other): + def __radd__(self, other: _Operand | float) -> _Operand: return self.apply("add", other, self) - def __sub__(self, other): + def __sub__(self, other: _Operand | float) -> _Operand: return self.apply("sub", self, other) - def __rsub__(self, other): + def __rsub__(self, other: _Operand | float) -> _Operand: return self.apply("sub", other, self) - def __mul__(self, other): + def __mul__(self, other: _Operand | float) -> _Operand: return self.apply("mul", self, other) - def __rmul__(self, other): + def __rmul__(self, other: _Operand | float) -> _Operand: return self.apply("mul", other, self) - def __truediv__(self, other): + def __truediv__(self, other: _Operand | float) -> _Operand: return self.apply("div", self, other) - def __rtruediv__(self, other): + def __rtruediv__(self, other: _Operand | float) -> _Operand: return self.apply("div", other, self) - def __mod__(self, other): + def __mod__(self, other: _Operand | float) -> _Operand: return self.apply("mod", self, other) - def __rmod__(self, other): + def __rmod__(self, other: _Operand | float) -> _Operand: return self.apply("mod", other, self) - def __pow__(self, other): + def __pow__(self, other: _Operand | float) -> _Operand: return self.apply("pow", self, other) - def __rpow__(self, other): + def __rpow__(self, other: _Operand | float) -> _Operand: return self.apply("pow", other, self) # bitwise - def __invert__(self): + def __invert__(self) -> _Operand: return self.apply("invert", self) - def __and__(self, other): + def __and__(self, other: _Operand | float) -> _Operand: return self.apply("and", self, other) - def __rand__(self, other): + def __rand__(self, other: _Operand | float) -> _Operand: return self.apply("and", other, self) - def __or__(self, other): + def __or__(self, other: _Operand | float) -> _Operand: return self.apply("or", self, other) - def __ror__(self, other): + def __ror__(self, other: _Operand | float) -> _Operand: return self.apply("or", other, self) - def __xor__(self, other): + def __xor__(self, other: _Operand | float) -> _Operand: return self.apply("xor", self, other) - def __rxor__(self, other): + def __rxor__(self, other: _Operand | float) -> _Operand: return self.apply("xor", other, self) - def __lshift__(self, other): + def __lshift__(self, other: _Operand | float) -> _Operand: return self.apply("lshift", self, other) - def __rshift__(self, other): + def __rshift__(self, other: _Operand | float) -> _Operand: return self.apply("rshift", self, other) # logical @@ -170,46 +180,46 @@ class _Operand: def __ne__(self, other): return self.apply("ne", self, other) - def __lt__(self, other): + def __lt__(self, other: _Operand | float) -> _Operand: return self.apply("lt", self, other) - def __le__(self, other): + def __le__(self, other: _Operand | float) -> _Operand: return self.apply("le", self, other) - def __gt__(self, other): + def __gt__(self, other: _Operand | float) -> _Operand: return self.apply("gt", self, other) - def __ge__(self, other): + def __ge__(self, other: _Operand | float) -> _Operand: return self.apply("ge", self, other) # conversions -def imagemath_int(self): +def imagemath_int(self: _Operand) -> _Operand: return _Operand(self.im.convert("I")) -def imagemath_float(self): +def imagemath_float(self: _Operand) -> _Operand: return _Operand(self.im.convert("F")) # logical -def imagemath_equal(self, other): +def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("eq", self, other, mode="I") -def imagemath_notequal(self, other): +def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("ne", self, other, mode="I") -def imagemath_min(self, other): +def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("min", self, other) -def imagemath_max(self, other): +def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("max", self, other) -def imagemath_convert(self, mode): +def imagemath_convert(self: _Operand, mode: str) -> _Operand: return _Operand(self.im.convert(mode)) @@ -219,7 +229,7 @@ for k, v in list(globals().items()): ops[k[10:]] = v -def eval(expression, _dict={}, **kw): +def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: """ Evaluates an image expression. @@ -247,7 +257,7 @@ def eval(expression, _dict={}, **kw): compiled_code = compile(expression, "", "eval") - def scan(code): + def scan(code) -> None: for const in code.co_consts: if type(const) is type(compiled_code): scan(const) diff --git a/src/PIL/_imagingmath.pyi b/src/PIL/_imagingmath.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imagingmath.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... From 38bfe3cddf618bf5aaaf0d5c88011031fd0eaf45 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:36:26 +1100 Subject: [PATCH 156/186] Added type hint Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0fbbe5861..ac13c6c0c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1305,7 +1305,7 @@ class Image: """ return ImageMode.getmode(self.mode).bands - def getbbox(self, *, alpha_only=True) -> tuple[int, int, int, int]: + def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: """ Calculates the bounding box of the non-zero regions in the image. From 993bc6c2027926633321593d5c39a29cbe926e3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 Jan 2024 23:41:09 +1100 Subject: [PATCH 157/186] Added type hint --- src/PIL/ImageMath.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 949fa45bb..bc3318c04 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,6 +17,7 @@ from __future__ import annotations import builtins +from types import CodeType from typing import Any from . import Image, _imagingmath @@ -257,7 +258,7 @@ def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: compiled_code = compile(expression, "", "eval") - def scan(code) -> None: + def scan(code: CodeType) -> None: for const in code.co_consts: if type(const) is type(compiled_code): scan(const) From 6f144d45b98d6c331da9fcd437542e224793b893 Mon Sep 17 00:00:00 2001 From: Erik Soma Date: Wed, 10 Jan 2024 16:03:42 -0500 Subject: [PATCH 158/186] Rename repeated seeks apng to reflect what it is rather than how it is used. --- ...ive_correct_info.png => different_durations.png} | Bin Tests/test_file_apng.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename Tests/images/apng/{repeated_seeks_give_correct_info.png => different_durations.png} (100%) diff --git a/Tests/images/apng/repeated_seeks_give_correct_info.png b/Tests/images/apng/different_durations.png similarity index 100% rename from Tests/images/apng/repeated_seeks_give_correct_info.png rename to Tests/images/apng/different_durations.png diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 340332258..e2c4569ce 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -692,7 +692,7 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat def test_apng_repeated_seeks_give_correct_info() -> None: - with Image.open("Tests/images/apng/repeated_seeks_give_correct_info.png") as im: + with Image.open("Tests/images/apng/different_durations.png") as im: for i in range(3): im.seek(0) assert im.info["duration"] == 4000 From 5347b471c69c400c6199c8cd200b5844169f7988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Thu, 11 Jan 2024 02:08:46 +0100 Subject: [PATCH 159/186] Update Tests/test_imagecms.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_imagecms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 9575b026d..810394e6f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -639,7 +639,7 @@ def test_rgb_lab(mode): assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) -def test_deprecation(): +def test_deprecation() -> None: with pytest.warns(DeprecationWarning): assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") with pytest.warns(DeprecationWarning): From 08992cf6b18cc3ad1dc9e75088b505450c21388b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Jan 2024 20:01:25 +1100 Subject: [PATCH 160/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c267ca472..887319dab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Deprecate ImageCms constants and versions() function #7702 + [nulano, radarhere] + - Added PerspectiveTransform #7699 [radarhere] From bc192557b8de105da5942108d33d643f83513976 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Jan 2024 23:11:12 +1100 Subject: [PATCH 161/186] Added type hints --- pyproject.toml | 1 - src/PIL/ImageMorph.py | 50 +++++++++++++++++++++++---------------- src/PIL/_imagingmorph.pyi | 5 ++++ 3 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 src/PIL/_imagingmorph.pyi diff --git a/pyproject.toml b/pyproject.toml index 54a4bcaec..8acfc0420 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,6 @@ exclude = [ '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', - '^src/PIL/ImageMorph.py$', '^src/PIL/ImageQt.py$', '^src/PIL/ImageShow.py$', '^src/PIL/ImImagePlugin.py$', diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 282e7d2a5..534c6291a 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -62,12 +62,14 @@ class LutBuilder: """ - def __init__(self, patterns=None, op_name=None): + def __init__( + self, patterns: list[str] | None = None, op_name: str | None = None + ) -> None: if patterns is not None: self.patterns = patterns else: self.patterns = [] - self.lut = None + self.lut: bytearray | None = None if op_name is not None: known_patterns = { "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], @@ -87,25 +89,27 @@ class LutBuilder: self.patterns = known_patterns[op_name] - def add_patterns(self, patterns): + def add_patterns(self, patterns: list[str]) -> None: self.patterns += patterns - def build_default_lut(self): + def build_default_lut(self) -> None: symbols = [0, 1] m = 1 << 4 # pos of current pixel self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) - def get_lut(self): + def get_lut(self) -> bytearray | None: return self.lut - def _string_permute(self, pattern, permutation): + def _string_permute(self, pattern: str, permutation: list[int]) -> str: """string_permute takes a pattern and a permutation and returns the string permuted according to the permutation list. """ assert len(permutation) == 9 return "".join(pattern[p] for p in permutation) - def _pattern_permute(self, basic_pattern, options, basic_result): + def _pattern_permute( + self, basic_pattern: str, options: str, basic_result: int + ) -> list[tuple[str, int]]: """pattern_permute takes a basic pattern and its result and clones the pattern according to the modifications described in the $options parameter. It returns a list of all cloned patterns.""" @@ -135,12 +139,13 @@ class LutBuilder: return patterns - def build_lut(self): + def build_lut(self) -> bytearray: """Compile all patterns into a morphology lut. TBD :Build based on (file) morphlut:modify_lut """ self.build_default_lut() + assert self.lut is not None patterns = [] # Parse and create symmetries of the patterns strings @@ -159,10 +164,10 @@ class LutBuilder: patterns += self._pattern_permute(pattern, options, result) # compile the patterns into regular expressions for speed - for i, pattern in enumerate(patterns): + compiled_patterns = [] + for pattern in patterns: p = pattern[0].replace(".", "X").replace("X", "[01]") - p = re.compile(p) - patterns[i] = (p, pattern[1]) + compiled_patterns.append((re.compile(p), pattern[1])) # Step through table and find patterns that match. # Note that all the patterns are searched. The last one @@ -172,8 +177,8 @@ class LutBuilder: bitpattern = bin(i)[2:] bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] - for p, r in patterns: - if p.match(bitpattern): + for pattern, r in compiled_patterns: + if pattern.match(bitpattern): self.lut[i] = [0, 1][r] return self.lut @@ -182,7 +187,12 @@ class LutBuilder: class MorphOp: """A class for binary morphological operators""" - def __init__(self, lut=None, op_name=None, patterns=None): + def __init__( + self, + lut: bytearray | None = None, + op_name: str | None = None, + patterns: list[str] | None = None, + ) -> None: """Create a binary morphological operator""" self.lut = lut if op_name is not None: @@ -190,7 +200,7 @@ class MorphOp: elif patterns is not None: self.lut = LutBuilder(patterns=patterns).build_lut() - def apply(self, image): + def apply(self, image: Image.Image): """Run a single morphological operation on an image Returns a tuple of the number of changed pixels and the @@ -206,7 +216,7 @@ class MorphOp: count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage - def match(self, image): + def match(self, image: Image.Image): """Get a list of coordinates matching the morphological operation on an image. @@ -221,7 +231,7 @@ class MorphOp: raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.im.id) - def get_on_pixels(self, image): + def get_on_pixels(self, image: Image.Image): """Get a list of all turned on pixels in a binary image Returns a list of tuples of (x,y) coordinates @@ -232,7 +242,7 @@ class MorphOp: raise ValueError(msg) return _imagingmorph.get_on_pixels(image.im.id) - def load_lut(self, filename): + def load_lut(self, filename: str) -> None: """Load an operator from an mrl file""" with open(filename, "rb") as f: self.lut = bytearray(f.read()) @@ -242,7 +252,7 @@ class MorphOp: msg = "Wrong size operator file!" raise Exception(msg) - def save_lut(self, filename): + def save_lut(self, filename: str) -> None: """Save an operator to an mrl file""" if self.lut is None: msg = "No operator loaded" @@ -250,6 +260,6 @@ class MorphOp: with open(filename, "wb") as f: f.write(self.lut) - def set_lut(self, lut): + def set_lut(self, lut: bytearray | None) -> None: """Set the lut from an external source""" self.lut = lut diff --git a/src/PIL/_imagingmorph.pyi b/src/PIL/_imagingmorph.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imagingmorph.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... From 067c5f4123c7cab7507fca02605bfd9762861d1f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Jan 2024 23:13:29 +1100 Subject: [PATCH 162/186] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 887319dab..62ae2a68b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Fix APNG info after seeking backwards more than twice #7701 + [esoma, radarhere] + - Deprecate ImageCms constants and versions() function #7702 [nulano, radarhere] From 10cf2f2651eee87c127bbb6d090138506c86fbc6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 14:40:59 +1100 Subject: [PATCH 163/186] Added type hints --- pyproject.toml | 1 - src/PIL/Image.py | 6 +++-- src/PIL/ImageShow.py | 60 ++++++++++++++++++++++++++------------------ tox.ini | 1 + 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8acfc0420..789df6f5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,7 +147,6 @@ exclude = [ '^src/PIL/FpxImagePlugin.py$', '^src/PIL/Image.py$', '^src/PIL/ImageQt.py$', - '^src/PIL/ImageShow.py$', '^src/PIL/ImImagePlugin.py$', '^src/PIL/MicImagePlugin.py$', '^src/PIL/PdfParser.py$', diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ac13c6c0c..5ab27c359 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -242,7 +242,7 @@ MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", " _MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") -def getmodebase(mode): +def getmodebase(mode: str) -> str: """ Gets the "base" mode for given mode. This function returns "L" for images that contain grayscale data, and "RGB" for images that @@ -583,7 +583,9 @@ class Image: else: self.load() - def _dump(self, file=None, format=None, **options): + def _dump( + self, file: str | None = None, format: str | None = None, **options + ) -> str: suffix = "" if format: suffix = "." + format diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index fad3e0980..d90545e92 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -13,18 +13,20 @@ # from __future__ import annotations +import abc import os import shutil import subprocess import sys from shlex import quote +from typing import Any from . import Image _viewers = [] -def register(viewer, order=1): +def register(viewer, order: int = 1) -> None: """ The :py:func:`register` function is used to register additional viewers:: @@ -49,7 +51,7 @@ def register(viewer, order=1): _viewers.insert(0, viewer) -def show(image, title=None, **options): +def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: r""" Display a given image. @@ -69,7 +71,7 @@ class Viewer: # main api - def show(self, image, **options): + def show(self, image: Image.Image, **options: Any) -> int: """ The main function for displaying an image. Converts the given image to the target format and displays it. @@ -87,16 +89,16 @@ class Viewer: # hook methods - format = None + format: str | None = None """The format to convert the image into.""" - options = {} + options: dict[str, Any] = {} """Additional options used to convert the image.""" - def get_format(self, image): + def get_format(self, image: Image.Image) -> str | None: """Return format name, or ``None`` to save as PGM/PPM.""" return self.format - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: """ Returns the command used to display the file. Not implemented in the base class. @@ -104,15 +106,15 @@ class Viewer: msg = "unavailable in base viewer" raise NotImplementedError(msg) - def save_image(self, image): + def save_image(self, image: Image.Image) -> str: """Save to temporary file and return filename.""" return image._dump(format=self.get_format(image), **self.options) - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> int: """Display the given image.""" return self.show_file(self.save_image(image), **options) - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -129,7 +131,7 @@ class WindowsViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: return ( f'start "Pillow" /WAIT "{file}" ' "&& ping -n 4 127.0.0.1 >NUL " @@ -147,14 +149,14 @@ class MacViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: # on darwin open returns immediately resulting in the temp # file removal while app is opening command = "open -a Preview.app" command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" return command - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -180,7 +182,11 @@ class UnixViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + @abc.abstractmethod + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + pass + + def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] return f"({command} {quote(file)}" @@ -190,11 +196,11 @@ class XDGViewer(UnixViewer): The freedesktop.org ``xdg-open`` command. """ - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: command = executable = "xdg-open" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -208,13 +214,15 @@ class DisplayViewer(UnixViewer): This viewer supports the ``title`` parameter. """ - def get_command_ex(self, file, title=None, **options): + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: command = executable = "display" if title: command += f" -title {quote(title)}" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -231,12 +239,12 @@ class DisplayViewer(UnixViewer): class GmDisplayViewer(UnixViewer): """The GraphicsMagick ``gm display`` command.""" - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: executable = "gm" command = "gm display" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -247,12 +255,12 @@ class GmDisplayViewer(UnixViewer): class EogViewer(UnixViewer): """The GNOME Image Viewer ``eog`` command.""" - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: executable = "eog" command = "eog -n" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -266,7 +274,9 @@ class XVViewer(UnixViewer): This viewer supports the ``title`` parameter. """ - def get_command_ex(self, file, title=None, **options): + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: # note: xv is pretty outdated. most modern systems have # imagemagick's display command instead. command = executable = "xv" @@ -274,7 +284,7 @@ class XVViewer(UnixViewer): command += f" -name {quote(title)}" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -304,7 +314,7 @@ if sys.platform not in ("win32", "darwin"): # unixoids class IPythonViewer(Viewer): """The viewer for IPython frontends.""" - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> int: ipython_display(image) return 1 diff --git a/tox.ini b/tox.ini index d89d017e4..fb6746ce7 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,7 @@ commands = [testenv:mypy] skip_install = true deps = + ipython mypy==1.7.1 numpy extras = From ffd0363b65ca05870f9306e8a6e999d58d9725ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jan 2024 16:26:15 +1100 Subject: [PATCH 164/186] Added type hints --- src/PIL/FitsImagePlugin.py | 8 +++++--- src/PIL/Image.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 7dce2d60f..e69890bab 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -15,7 +15,7 @@ import math from . import Image, ImageFile -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] == b"SIMPLE" @@ -23,8 +23,10 @@ class FitsImageFile(ImageFile.ImageFile): format = "FITS" format_description = "FITS" - def _open(self): - headers = {} + def _open(self) -> None: + assert self.fp is not None + + headers: dict[bytes, bytes] = {} while True: header = self.fp.read(80) if not header: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ac13c6c0c..b2520d57c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3484,7 +3484,7 @@ def register_extension(id, extension) -> None: EXTENSION[extension.lower()] = id.upper() -def register_extensions(id, extensions): +def register_extensions(id, extensions) -> None: """ Registers image extensions. This function should not be used in application code. From c97b5c6f7a221ed534d93e943992e44c2a7ec5fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 Jan 2024 22:29:56 +1100 Subject: [PATCH 165/186] Exclude abstract method code from coverage --- src/PIL/ImageShow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index d90545e92..c03122c11 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -184,7 +184,7 @@ class UnixViewer(Viewer): @abc.abstractmethod def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass + pass # pragma: no cover def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] From c75a93b9a35e0835cc44465d5a73a9a05be0d166 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Jan 2024 16:11:48 +1100 Subject: [PATCH 166/186] Added type hints --- src/PIL/Image.py | 4 ++-- src/PIL/ImageFile.py | 2 +- src/PIL/ImagePalette.py | 2 +- src/PIL/McIdasImagePlugin.py | 8 +++++--- src/PIL/PcdImagePlugin.py | 8 ++++++-- src/PIL/PcxImagePlugin.py | 10 +++++++--- src/PIL/PixarImagePlugin.py | 6 ++++-- src/PIL/SunImagePlugin.py | 6 ++++-- src/PIL/XVThumbImagePlugin.py | 6 ++++-- src/PIL/XbmImagePlugin.py | 9 ++++++--- 10 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c1f89af46..e32f254de 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3437,7 +3437,7 @@ def register_open(id, factory, accept=None) -> None: OPEN[id] = factory, accept -def register_mime(id, mimetype): +def register_mime(id, mimetype) -> None: """ Registers an image MIME type by populating ``Image.MIME``. This function should not be used in application code. @@ -3452,7 +3452,7 @@ def register_mime(id, mimetype): MIME[id.upper()] = mimetype -def register_save(id, driver): +def register_save(id, driver) -> None: """ Registers an image save function. This function should not be used in application code. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0923979af..72c3c03c5 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -514,7 +514,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im, fp, tile, bufsize=0): +def _save(im, fp, tile, bufsize=0) -> None: """Helper to save image based on tile list :param im: Image object. diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fbcfa309d..2b6cecc61 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -192,7 +192,7 @@ class ImagePalette: # Internal -def raw(rawmode, data): +def raw(rawmode, data) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 9a85c0d15..27972236c 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -22,8 +22,8 @@ import struct from . import Image, ImageFile -def _accept(s): - return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" +def _accept(prefix: bytes) -> bool: + return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" ## @@ -34,8 +34,10 @@ class McIdasImageFile(ImageFile.ImageFile): format = "MCIDAS" format_description = "McIdas area file" - def _open(self): + def _open(self) -> None: # parse area file directory + assert self.fp is not None + s = self.fp.read(256) if not _accept(s) or len(s) != 256: msg = "not an McIdas area file" diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index a0515b302..1cd5c4a9d 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -27,8 +27,10 @@ class PcdImageFile(ImageFile.ImageFile): format = "PCD" format_description = "Kodak PhotoCD" - def _open(self): + def _open(self) -> None: # rough + assert self.fp is not None + self.fp.seek(2048) s = self.fp.read(2048) @@ -47,9 +49,11 @@ class PcdImageFile(ImageFile.ImageFile): self._size = 768, 512 # FIXME: not correct for rotated images! self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] - def load_end(self): + def load_end(self) -> None: if self.tile_post_rotate: # Handle rotated PCDs + assert self.im is not None + self.im = self.im.rotate(self.tile_post_rotate) self._size = self.im.size diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 98ecefd05..3e0968a83 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -37,7 +37,7 @@ from ._binary import o16le as o16 logger = logging.getLogger(__name__) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] @@ -49,8 +49,10 @@ class PcxImageFile(ImageFile.ImageFile): format = "PCX" format_description = "Paintbrush" - def _open(self): + def _open(self) -> None: # header + assert self.fp is not None + s = self.fp.read(128) if not _accept(s): msg = "not a PCX file" @@ -141,7 +143,7 @@ SAVE = { } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: @@ -199,6 +201,8 @@ def _save(im, fp, filename): if im.mode == "P": # colour palette + assert im.im is not None + fp.write(o8(12)) palette = im.im.getpalette("RGB", "RGB") palette += b"\x00" * (768 - len(palette)) diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index af866feb3..887b6568b 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -27,7 +27,7 @@ from ._binary import i16le as i16 # helpers -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"\200\350\000\000" @@ -39,8 +39,10 @@ class PixarImageFile(ImageFile.ImageFile): format = "PIXAR" format_description = "PIXAR raster image" - def _open(self): + def _open(self) -> None: # assuming a 4-byte magic label + assert self.fp is not None + s = self.fp.read(4) if not _accept(s): msg = "not a PIXAR file" diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 11ce3dfef..4e098474a 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -21,7 +21,7 @@ from . import Image, ImageFile, ImagePalette from ._binary import i32be as i32 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 @@ -33,7 +33,7 @@ class SunImageFile(ImageFile.ImageFile): format = "SUN" format_description = "Sun Raster File" - def _open(self): + def _open(self) -> None: # The Sun Raster file header is 32 bytes in length # and has the following format: @@ -49,6 +49,8 @@ class SunImageFile(ImageFile.ImageFile): # DWORD ColorMapLength; /* Size of the color map in bytes */ # } SUNRASTER; + assert self.fp is not None + # HEAD s = self.fp.read(32) if not _accept(s): diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 47ba1c548..c84adaca2 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -33,7 +33,7 @@ for r in range(8): ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] == _MAGIC @@ -45,8 +45,10 @@ class XVThumbImageFile(ImageFile.ImageFile): format = "XVThumb" format_description = "XV thumbnail image" - def _open(self): + def _open(self) -> None: # check magic + assert self.fp is not None + if not _accept(self.fp.read(6)): msg = "not an XV thumbnail file" raise SyntaxError(msg) diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 566acbfe5..0291e2858 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -21,6 +21,7 @@ from __future__ import annotations import re +from io import BytesIO from . import Image, ImageFile @@ -36,7 +37,7 @@ xbm_head = re.compile( ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix.lstrip()[:7] == b"#define" @@ -48,7 +49,9 @@ class XbmImageFile(ImageFile.ImageFile): format = "XBM" format_description = "X11 Bitmap" - def _open(self): + def _open(self) -> None: + assert self.fp is not None + m = xbm_head.match(self.fp.read(512)) if not m: @@ -67,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile): self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) From 575edbefe49641107b7315b3113e35c6b74f9bca Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:25:31 +1100 Subject: [PATCH 167/186] Added type hints Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e32f254de..3f35bf50e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3437,7 +3437,7 @@ def register_open(id, factory, accept=None) -> None: OPEN[id] = factory, accept -def register_mime(id, mimetype) -> None: +def register_mime(id: str, mimetype: str) -> None: """ Registers an image MIME type by populating ``Image.MIME``. This function should not be used in application code. @@ -3452,7 +3452,7 @@ def register_mime(id, mimetype) -> None: MIME[id.upper()] = mimetype -def register_save(id, driver) -> None: +def register_save(id: str, driver) -> None: """ Registers an image save function. This function should not be used in application code. From 4a6cb0f8447869ce886db1cde088723b55df32a2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Jan 2024 20:04:51 +1100 Subject: [PATCH 168/186] Added type hints --- src/PIL/ImtImagePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 7469c592d..abb3fb762 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -33,10 +33,12 @@ class ImtImageFile(ImageFile.ImageFile): format = "IMT" format_description = "IM Tools" - def _open(self): + def _open(self) -> None: # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. + assert self.fp is not None + buffer = self.fp.read(100) if b"\n" not in buffer: msg = "not an IM file" From 6a2bdb6feb921fae4160c26e63e2ff541c4a7738 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 09:00:40 +1100 Subject: [PATCH 169/186] Added type hints --- src/PIL/Image.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/MspImagePlugin.py | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3f35bf50e..b7bb514ac 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3507,7 +3507,7 @@ def registered_extensions(): return EXTENSION -def register_decoder(name, decoder): +def register_decoder(name: str, decoder) -> None: """ Registers an image decoder. This function should not be used in application code. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 72c3c03c5..b79f2707b 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -713,7 +713,7 @@ class PyDecoder(PyCodec): msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data, rawmode=None): + def set_as_raw(self, data: bytes, rawmode = None) -> None: """ Convenience method to set the internal image from a stream of raw data diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 77dac65b6..bb7e466a7 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -35,7 +35,7 @@ from ._binary import o16le as o16 # read MSP files -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] in [b"DanM", b"LinS"] @@ -48,8 +48,10 @@ class MspImageFile(ImageFile.ImageFile): format = "MSP" format_description = "Windows Paint" - def _open(self): + def _open(self) -> None: # Header + assert self.fp is not None + s = self.fp.read(32) if not _accept(s): msg = "not an MSP file" @@ -109,7 +111,9 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + img = io.BytesIO() blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: @@ -159,7 +163,7 @@ Image.register_decoder("MSP", MspDecoder) # write MSP files (uncompressed only) -def _save(im, fp, filename): +def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) From edaf7acdb3581e20cc7e77b631db736782cd8a15 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:01:17 +0000 Subject: [PATCH 170/186] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b79f2707b..40353da67 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -713,7 +713,7 @@ class PyDecoder(PyCodec): msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data: bytes, rawmode = None) -> None: + def set_as_raw(self, data: bytes, rawmode=None) -> None: """ Convenience method to set the internal image from a stream of raw data From 5a587193c7ed360cc0705ae81c4628e990420384 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 12:22:59 +1100 Subject: [PATCH 171/186] Added type hints --- src/PIL/Image.py | 8 ++++---- src/PIL/SgiImagePlugin.py | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b7bb514ac..ec1cff896 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -710,7 +710,7 @@ class Image: self.putpalette(palette) self.frombytes(data) - def tobytes(self, encoder_name="raw", *args): + def tobytes(self, encoder_name: str = "raw", *args) -> bytes: """ Return image as a bytes object. @@ -788,7 +788,7 @@ class Image: ] ) - def frombytes(self, data, decoder_name="raw", *args): + def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: """ Loads this image with pixel data from a bytes object. @@ -1297,7 +1297,7 @@ class Image: ] return merge(self.mode, ims) - def getbands(self): + def getbands(self) -> tuple[str, ...]: """ Returns a tuple containing the name of each band in this image. For example, ``getbands`` on an RGB image returns ("R", "G", "B"). @@ -2495,7 +2495,7 @@ class Image: _show(self, title=title) - def split(self): + def split(self) -> tuple[Image, ...]: """ Split this image into individual bands. This method returns a tuple of individual image bands from an image. For example, diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index f9a10f610..ccf661ff1 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,13 +24,14 @@ from __future__ import annotations import os import struct +from io import BytesIO from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import o8 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 2 and i16(prefix) == 474 @@ -52,8 +53,10 @@ class SgiImageFile(ImageFile.ImageFile): format = "SGI" format_description = "SGI Image File Format" - def _open(self): + def _open(self) -> None: # HEAD + assert self.fp is not None + headlen = 512 s = self.fp.read(headlen) @@ -122,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile): ] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -168,8 +171,8 @@ def _save(im, fp, filename): # Maximum Byte value (255 = 8bits per pixel) pinmax = 255 # Image name (79 characters max, truncated below in write) - img_name = os.path.splitext(os.path.basename(filename))[0] - img_name = img_name.encode("ascii", "ignore") + filename = os.path.basename(filename) + img_name = os.path.splitext(filename)[0].encode("ascii", "ignore") # Standard representation of pixel in the file colormap = 0 fp.write(struct.pack(">h", magic_number)) @@ -201,7 +204,10 @@ def _save(im, fp, filename): class SGI16Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + assert self.im is not None + rawmode, stride, orientation = self.args pagesize = self.state.xsize * self.state.ysize zsize = len(self.mode) From e2aa0fd4996b85334df3831c2df9bf2d5f68dadd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jan 2024 12:55:48 +1100 Subject: [PATCH 172/186] Changed ops to be static --- src/PIL/ImageMath.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index bc3318c04..a7652f237 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -224,10 +224,15 @@ def imagemath_convert(self: _Operand, mode: str) -> _Operand: return _Operand(self.im.convert(mode)) -ops = {} -for k, v in list(globals().items()): - if k[:10] == "imagemath_": - ops[k[10:]] = v +ops = { + "int": imagemath_int, + "float": imagemath_float, + "equal": imagemath_equal, + "notequal": imagemath_notequal, + "min": imagemath_min, + "max": imagemath_max, + "convert": imagemath_convert, +} def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: @@ -244,7 +249,7 @@ def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: """ # build execution namespace - args = ops.copy() + args: dict[str, Any] = ops.copy() for k in list(_dict.keys()) + list(kw.keys()): if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" From 54c96df9d6e370d16960ee8a2aee932b41f213f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 08:03:09 +1100 Subject: [PATCH 173/186] Added type hints --- src/PIL/TgaImagePlugin.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 65c7484f7..584932d2c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import warnings +from io import BytesIO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -49,8 +50,10 @@ class TgaImageFile(ImageFile.ImageFile): format = "TGA" format_description = "Targa" - def _open(self): + def _open(self) -> None: # process header + assert self.fp is not None + s = self.fp.read(18) id_len = s[0] @@ -151,8 +154,9 @@ class TgaImageFile(ImageFile.ImageFile): except KeyError: pass # cannot decode - def load_end(self): + def load_end(self) -> None: if self._flip_horizontally: + assert self.im is not None self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) @@ -171,7 +175,7 @@ SAVE = { } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: @@ -194,6 +198,7 @@ def _save(im, fp, filename): warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: + assert im.im is not None palette = im.im.getpalette("RGB", "BGR") colormaplength, colormapentry = len(palette) // 3, 24 else: From 7972332bc59800aa64c23664645fe9f8277cdbf4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 19:22:45 +1100 Subject: [PATCH 174/186] Added type hints --- src/PIL/Image.py | 2 +- src/PIL/ImageFile.py | 2 ++ src/PIL/PpmImagePlugin.py | 46 +++++++++++++++++++++++++-------------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ec1cff896..553f36703 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -282,7 +282,7 @@ def getmodebandnames(mode): return ImageMode.getmode(mode).bands -def getmodebands(mode): +def getmodebands(mode: str) -> int: """ Gets the number of individual bands for this mode. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 40353da67..5ba5a6f82 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -616,6 +616,8 @@ class PyCodecState: class PyCodec: + fd: io.BytesIO | None + def __init__(self, mode, *args): self.im = None self.state = PyCodecState() diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 9d37dcde0..3e45ba95c 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -16,6 +16,7 @@ from __future__ import annotations import math +from io import BytesIO from . import Image, ImageFile from ._binary import i16be as i16 @@ -45,7 +46,7 @@ MODES = { } -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" @@ -57,7 +58,9 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _read_magic(self): + def _read_magic(self) -> bytes: + assert self.fp is not None + magic = b"" # read until whitespace or longest available magic number for _ in range(6): @@ -67,7 +70,9 @@ class PpmImageFile(ImageFile.ImageFile): magic += c return magic - def _read_token(self): + def _read_token(self) -> bytes: + assert self.fp is not None + token = b"" while len(token) <= 10: # read until next whitespace or limit of 10 characters c = self.fp.read(1) @@ -93,7 +98,9 @@ class PpmImageFile(ImageFile.ImageFile): raise ValueError(msg) return token - def _open(self): + def _open(self) -> None: + assert self.fp is not None + magic_number = self._read_magic() try: mode = MODES[magic_number] @@ -114,6 +121,8 @@ class PpmImageFile(ImageFile.ImageFile): decoder_name = "raw" if magic_number in (b"P1", b"P2", b"P3"): decoder_name = "ppm_plain" + + args: str | tuple[str | int, ...] if mode == "1": args = "1;I" elif mode == "F": @@ -151,16 +160,19 @@ class PpmImageFile(ImageFile.ImageFile): class PpmPlainDecoder(ImageFile.PyDecoder): _pulls_fd = True + _comment_spans: bool + + def _read_block(self) -> bytes: + assert self.fd is not None - def _read_block(self): return self.fd.read(ImageFile.SAFEBLOCK) - def _find_comment_end(self, block, start=0): + def _find_comment_end(self, block: bytes, start: int = 0) -> int: a = block.find(b"\n", start) b = block.find(b"\r", start) return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) - def _ignore_comments(self, block): + def _ignore_comments(self, block: bytes) -> bytes: if self._comment_spans: # Finish current comment while block: @@ -194,7 +206,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): break return block - def _decode_bitonal(self): + def _decode_bitonal(self) -> bytearray: """ This is a separate method because in the plain PBM format, all data tokens are exactly one byte, so the inter-token whitespace is optional. @@ -219,7 +231,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) - def _decode_blocks(self, maxval): + def _decode_blocks(self, maxval: int) -> bytearray: data = bytearray() max_len = 10 out_byte_count = 4 if self.mode == "I" else 1 @@ -227,7 +239,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): bands = Image.getmodebands(self.mode) total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count - half_token = False + half_token = b"" while len(data) != total_bytes: block = self._read_block() # read next block if not block: @@ -241,7 +253,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): if half_token: block = half_token + block # stitch half_token to new block - half_token = False + half_token = b"" tokens = block.split() @@ -259,15 +271,15 @@ class PpmPlainDecoder(ImageFile.PyDecoder): raise ValueError(msg) value = int(token) if value > maxval: - msg = f"Channel value too large for this mode: {value}" - raise ValueError(msg) + msg_str = f"Channel value too large for this mode: {value}" + raise ValueError(msg_str) value = round(value / maxval * out_max) data += o32(value) if self.mode == "I" else o8(value) if len(data) == total_bytes: # finished! break return data - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: self._comment_spans = False if self.mode == "1": data = self._decode_bitonal() @@ -283,7 +295,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder): class PpmDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + data = bytearray() maxval = self.args[-1] in_byte_count = 1 if maxval < 256 else 2 @@ -310,7 +324,7 @@ class PpmDecoder(ImageFile.PyDecoder): # -------------------------------------------------------------------- -def _save(im, fp, filename): +def _save(im: Image.Image, fp: BytesIO, filename: str) -> None: if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": From d8c7af0157269f82106216788e9073b4379f786d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jan 2024 23:10:37 +1100 Subject: [PATCH 175/186] Added type hints to GdImageFile --- src/PIL/GdImageFile.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index d84876eb6..7bb4736af 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,6 +27,8 @@ """ from __future__ import annotations +from io import BytesIO + from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 @@ -43,8 +45,10 @@ class GdImageFile(ImageFile.ImageFile): format = "GD" format_description = "GD uncompressed images" - def _open(self): + def _open(self) -> None: # Header + assert self.fp is not None + s = self.fp.read(1037) if i16(s) not in [65534, 65535]: @@ -76,7 +80,7 @@ class GdImageFile(ImageFile.ImageFile): ] -def open(fp, mode="r"): +def open(fp: BytesIO, mode: str = "r") -> GdImageFile: """ Load texture from a GD image file. From 6a85653cc373fd78fdf04e53d3fc38b6d83a7e1b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Jan 2024 12:05:54 +1100 Subject: [PATCH 176/186] Added type hints --- src/PIL/MpegImagePlugin.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index f4e598ca3..b9e9243e5 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -14,6 +14,8 @@ # from __future__ import annotations +from io import BytesIO + from . import Image, ImageFile from ._binary import i8 @@ -22,15 +24,15 @@ from ._binary import i8 class BitStream: - def __init__(self, fp): + def __init__(self, fp: BytesIO) -> None: self.fp = fp self.bits = 0 self.bitbuffer = 0 - def next(self): + def next(self) -> int: return i8(self.fp.read(1)) - def peek(self, bits): + def peek(self, bits: int) -> int: while self.bits < bits: c = self.next() if c < 0: @@ -40,13 +42,13 @@ class BitStream: self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 - def skip(self, bits): + def skip(self, bits: int) -> None: while self.bits < bits: self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) self.bits += 8 self.bits = self.bits - bits - def read(self, bits): + def read(self, bits: int) -> int: v = self.peek(bits) self.bits = self.bits - bits return v @@ -61,9 +63,10 @@ class MpegImageFile(ImageFile.ImageFile): format = "MPEG" format_description = "MPEG" - def _open(self): - s = BitStream(self.fp) + def _open(self) -> None: + assert self.fp is not None + s = BitStream(self.fp) if s.read(32) != 0x1B3: msg = "not an MPEG file" raise SyntaxError(msg) From 81b5c5dc68be449478442364a86e391906a61228 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Jan 2024 08:37:58 +1100 Subject: [PATCH 177/186] Added type hints --- Tests/helper.py | 2 +- Tests/oss-fuzz/fuzz_font.py | 4 ++-- Tests/oss-fuzz/fuzz_pillow.py | 4 ++-- Tests/oss-fuzz/fuzzers.py | 8 ++++---- Tests/oss-fuzz/test_fuzzers.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index b333c2fd4..88c1f02a8 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -158,7 +158,7 @@ def assert_tuple_approx_equal(actuals, targets, threshold, msg): assert value, msg + ": " + repr(actuals) + " != " + repr(targets) -def skip_unless_feature(feature): +def skip_unless_feature(feature: str) -> pytest.MarkDecorator: reason = f"{feature} not available" return pytest.mark.skipif(not features.check(feature), reason=reason) diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index bc2ba9a7e..8788d7021 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -23,7 +23,7 @@ with atheris.instrument_imports(): import fuzzers -def TestOneInput(data): +def TestOneInput(data: bytes) -> None: try: fuzzers.fuzz_font(data) except Exception: @@ -32,7 +32,7 @@ def TestOneInput(data): pass -def main(): +def main() -> None: fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index 545daccb6..e6e99d415 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -23,7 +23,7 @@ with atheris.instrument_imports(): import fuzzers -def TestOneInput(data): +def TestOneInput(data: bytes) -> None: try: fuzzers.fuzz_image(data) except Exception: @@ -32,7 +32,7 @@ def TestOneInput(data): pass -def main(): +def main() -> None: fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 3f3c1e388..3afa95215 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -5,18 +5,18 @@ import warnings from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont -def enable_decompressionbomb_error(): +def enable_decompressionbomb_error() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True warnings.filterwarnings("ignore") warnings.simplefilter("error", Image.DecompressionBombWarning) -def disable_decompressionbomb_error(): +def disable_decompressionbomb_error() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = False warnings.resetwarnings() -def fuzz_image(data): +def fuzz_image(data: bytes) -> None: # This will fail on some images in the corpus, as we have many # invalid images in the test suite. with Image.open(io.BytesIO(data)) as im: @@ -25,7 +25,7 @@ def fuzz_image(data): im.save(io.BytesIO(), "BMP") -def fuzz_font(data): +def fuzz_font(data: bytes) -> None: wrapper = io.BytesIO(data) try: font = ImageFont.truetype(wrapper) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a..028ee71ee 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -23,7 +23,7 @@ if features.check("libjpeg_turbo"): "path", subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), ) -def test_fuzz_images(path): +def test_fuzz_images(path: str) -> None: fuzzers.enable_decompressionbomb_error() try: with open(path, "rb") as f: @@ -54,7 +54,7 @@ def test_fuzz_images(path): @pytest.mark.parametrize( "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") ) -def test_fuzz_fonts(path): +def test_fuzz_fonts(path: str) -> None: if not path: return with open(path, "rb") as f: From 9b6c1e3763000329e80173b99507e10aa0569b43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jan 2024 11:13:06 +1100 Subject: [PATCH 178/186] Added type hints --- Tests/helper.py | 157 +++++++++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 68 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 88c1f02a8..12a4fed38 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -14,47 +14,46 @@ from io import BytesIO import pytest from packaging.version import parse as parse_version +from typing import Any, Callable, Sequence from PIL import Image, ImageMath, features logger = logging.getLogger(__name__) - -HAS_UPLOADER = False - +uploader = None if os.environ.get("SHOW_ERRORS"): - # local img.show for errors. - HAS_UPLOADER = True - - class test_image_results: - @staticmethod - def upload(a, b): - a.show() - b.show() - + uploader = "show" elif "GITHUB_ACTIONS" in os.environ: - HAS_UPLOADER = True - - class test_image_results: - @staticmethod - def upload(a, b): - dir_errors = os.path.join(os.path.dirname(__file__), "errors") - os.makedirs(dir_errors, exist_ok=True) - tmpdir = tempfile.mkdtemp(dir=dir_errors) - a.save(os.path.join(tmpdir, "a.png")) - b.save(os.path.join(tmpdir, "b.png")) - return tmpdir - + uploader = "github_actions" else: try: import test_image_results - HAS_UPLOADER = True + uploader = "aws" except ImportError: pass -def convert_to_comparable(a, b): +def upload(a: Image.Image, b: Image.Image) -> str | None: + if uploader == "show": + # local img.show for errors. + a.show() + b.show() + elif uploader == "github_actions": + dir_errors = os.path.join(os.path.dirname(__file__), "errors") + os.makedirs(dir_errors, exist_ok=True) + tmpdir = tempfile.mkdtemp(dir=dir_errors) + a.save(os.path.join(tmpdir, "a.png")) + b.save(os.path.join(tmpdir, "b.png")) + return tmpdir + elif uploader == "aws": + return test_image_results.upload(a, b) + return None + + +def convert_to_comparable( + a: Image.Image, b: Image.Image +) -> tuple[Image.Image, Image.Image]: new_a, new_b = a, b if a.mode == "P": new_a = Image.new("L", a.size) @@ -67,14 +66,16 @@ def convert_to_comparable(a, b): return new_a, new_b -def assert_deep_equal(a, b, msg=None): +def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: assert a == b, msg -def assert_image(im, mode, size, msg=None): +def assert_image( + im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None +) -> None: if mode is not None: assert im.mode == mode, ( msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" @@ -86,13 +87,13 @@ def assert_image(im, mode, size, msg=None): ) -def assert_image_equal(a, b, msg=None): +def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None: assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" if a.tobytes() != b.tobytes(): - if HAS_UPLOADER: + if uploader: try: - url = test_image_results.upload(a, b) + url = upload(a, b) logger.error("URL for test images: %s", url) except Exception: pass @@ -100,14 +101,18 @@ def assert_image_equal(a, b, msg=None): pytest.fail(msg or "got different content") -def assert_image_equal_tofile(a, filename, msg=None, mode=None): +def assert_image_equal_tofile( + a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None +) -> None: with Image.open(filename) as img: if mode: img = img.convert(mode) assert_image_equal(a, img, msg) -def assert_image_similar(a, b, epsilon, msg=None): +def assert_image_similar( + a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None +) -> None: assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -125,37 +130,43 @@ def assert_image_similar(a, b, epsilon, msg=None): + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" ) except Exception as e: - if HAS_UPLOADER: + if uploader: try: - url = test_image_results.upload(a, b) + url = upload(a, b) logger.exception("URL for test images: %s", url) except Exception: pass raise e -def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None): +def assert_image_similar_tofile( + a: Image.Image, + filename: str, + epsilon: float, + msg: str | None = None, + mode: str | None = None, +) -> None: with Image.open(filename) as img: if mode: img = img.convert(mode) assert_image_similar(a, img, epsilon, msg) -def assert_all_same(items, msg=None): +def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) == len(items), msg -def assert_not_all_same(items, msg=None): +def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) != len(items), msg -def assert_tuple_approx_equal(actuals, targets, threshold, msg): +def assert_tuple_approx_equal( + actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str +) -> None: """Tests if actuals has values within threshold from targets""" - value = True for i, target in enumerate(targets): - value *= target - threshold <= actuals[i] <= target + threshold - - assert value, msg + ": " + repr(actuals) + " != " + repr(targets) + if not (target - threshold <= actuals[i] <= target + threshold): + pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) def skip_unless_feature(feature: str) -> pytest.MarkDecorator: @@ -163,17 +174,24 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator: return pytest.mark.skipif(not features.check(feature), reason=reason) -def skip_unless_feature_version(feature, version_required, reason=None): +def skip_unless_feature_version( + feature: str, required: str, reason: str | None = None +) -> pytest.MarkDecorator: if not features.check(feature): return pytest.mark.skip(f"{feature} not available") if reason is None: - reason = f"{feature} is older than {version_required}" - version_required = parse_version(version_required) + reason = f"{feature} is older than {required}" + version_required = parse_version(required) version_available = parse_version(features.version(feature)) return pytest.mark.skipif(version_available < version_required, reason=reason) -def mark_if_feature_version(mark, feature, version_blacklist, reason=None): +def mark_if_feature_version( + mark: pytest.MarkDecorator, + feature: str, + version_blacklist: str, + reason: str | None = None, +) -> pytest.MarkDecorator: if not features.check(feature): return pytest.mark.pil_noop_mark() if reason is None: @@ -194,7 +212,7 @@ class PillowLeakTestCase: iterations = 100 # count mem_limit = 512 # k - def _get_mem_usage(self): + def _get_mem_usage(self) -> float: """ Gets the RUSAGE memory usage, returns in K. Encapsulates the difference between macOS and Linux rss reporting @@ -216,7 +234,7 @@ class PillowLeakTestCase: # This is the maximum resident set size used (in kilobytes). return mem # Kb - def _test_leak(self, core): + def _test_leak(self, core: Callable[[], None]) -> None: start_mem = self._get_mem_usage() for cycle in range(self.iterations): core() @@ -228,17 +246,17 @@ class PillowLeakTestCase: # helpers -def fromstring(data): +def fromstring(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def tostring(im, string_format, **options): +def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes: out = BytesIO() im.save(out, string_format, **options) return out.getvalue() -def hopper(mode=None, cache={}): +def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image: if mode is None: # Always return fresh not-yet-loaded version of image. # Operations on not-yet-loaded images is separate class of errors @@ -259,29 +277,31 @@ def hopper(mode=None, cache={}): return im.copy() -def djpeg_available(): +def djpeg_available() -> bool: if shutil.which("djpeg"): try: subprocess.check_call(["djpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - return False + pass + return False -def cjpeg_available(): +def cjpeg_available() -> bool: if shutil.which("cjpeg"): try: subprocess.check_call(["cjpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - return False + pass + return False -def netpbm_available(): +def netpbm_available() -> bool: return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) -def magick_command(): +def magick_command() -> list[str] | None: if sys.platform == "win32": magickhome = os.environ.get("MAGICK_HOME") if magickhome: @@ -298,47 +318,48 @@ def magick_command(): return imagemagick if graphicsmagick and shutil.which(graphicsmagick[0]): return graphicsmagick + return None -def on_appveyor(): +def on_appveyor() -> bool: return "APPVEYOR" in os.environ -def on_github_actions(): +def on_github_actions() -> bool: return "GITHUB_ACTIONS" in os.environ -def on_ci(): +def on_ci() -> bool: # GitHub Actions and AppVeyor have "CI" return "CI" in os.environ -def is_big_endian(): +def is_big_endian() -> bool: return sys.byteorder == "big" -def is_ppc64le(): +def is_ppc64le() -> bool: import platform return platform.machine() == "ppc64le" -def is_win32(): +def is_win32() -> bool: return sys.platform.startswith("win32") -def is_pypy(): +def is_pypy() -> bool: return hasattr(sys, "pypy_translation_info") -def is_mingw(): +def is_mingw() -> bool: return sysconfig.get_platform() == "mingw" class CachedProperty: - def __init__(self, func): + def __init__(self, func: Callable[[Any], None]) -> None: self.func = func - def __get__(self, instance, cls=None): + def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any: result = instance.__dict__[self.func.__name__] = self.func(instance) return result From 5dc3de7974833bc8a0e2ecb389c6c183dd1a1d21 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 06:07:48 +0000 Subject: [PATCH 179/186] Update actions/cache action to v4 --- .github/workflows/lint.yml | 2 +- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9069fc615..cc4760288 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - name: pre-commit cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7244315ac..9c3eb0924 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -95,7 +95,7 @@ jobs: python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: 'C:\cygwin\home\runneradmin\.cache\pip' key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 372f97fd6..8cad7a8b2 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -89,7 +89,7 @@ jobs: - name: Cache build id: build-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: winbuild\build key: From f7701e6596c9a2e7c9ed886621f01b3861d70990 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jan 2024 21:04:57 +1100 Subject: [PATCH 180/186] Do not log URL of test images if there is no URL --- Tests/helper.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 12a4fed38..670c93633 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -91,12 +91,12 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) - assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" if a.tobytes() != b.tobytes(): - if uploader: - try: - url = upload(a, b) + try: + url = upload(a, b) + if url: logger.error("URL for test images: %s", url) - except Exception: - pass + except Exception: + pass pytest.fail(msg or "got different content") @@ -130,12 +130,12 @@ def assert_image_similar( + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" ) except Exception as e: - if uploader: - try: - url = upload(a, b) + try: + url = upload(a, b) + if url: logger.exception("URL for test images: %s", url) - except Exception: - pass + except Exception: + pass raise e From 99d851957fec5394bbc9d61ceff57f534bd1b873 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:23:08 +1100 Subject: [PATCH 181/186] Return early Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 670c93633..e4ed9c551 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -283,7 +283,7 @@ def djpeg_available() -> bool: subprocess.check_call(["djpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - pass + return False return False @@ -293,7 +293,7 @@ def cjpeg_available() -> bool: subprocess.check_call(["cjpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover - pass + return False return False From 970bd102ba0304adeb5351cc09c616ab23535d2c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:24:34 +1100 Subject: [PATCH 182/186] Updated type hint Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index e4ed9c551..4d2d27226 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -66,7 +66,7 @@ def convert_to_comparable( return new_a, new_b -def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None: +def assert_deep_equal(a: Sequence[Any], b: Sequence[Any], msg: str | None = None) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: From 9454c28f0f8363d1316b1dfec0f23404f9e36082 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:24:54 +0000 Subject: [PATCH 183/186] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/helper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index 4d2d27226..203d20053 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -66,7 +66,9 @@ def convert_to_comparable( return new_a, new_b -def assert_deep_equal(a: Sequence[Any], b: Sequence[Any], msg: str | None = None) -> None: +def assert_deep_equal( + a: Sequence[Any], b: Sequence[Any], msg: str | None = None +) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: From a18cee35ff4362c834d0fa43277ee5b02ab9c693 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:26:31 +1100 Subject: [PATCH 184/186] Updated import order Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/helper.py b/Tests/helper.py index 203d20053..99170c765 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -11,10 +11,10 @@ import sys import sysconfig import tempfile from io import BytesIO +from typing import Any, Callable, Sequence import pytest from packaging.version import parse as parse_version -from typing import Any, Callable, Sequence from PIL import Image, ImageMath, features From eba0be98ecea7150c691e05bf6faa3c73c121689 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:22:04 +0200 Subject: [PATCH 185/186] isort Tests --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 789df6f5e..b1ce9cf1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,7 +119,6 @@ extend-ignore = [ ] [tool.ruff.per-file-ignores] -"Tests/*.py" = ["I001"] "Tests/oss-fuzz/fuzz_font.py" = ["I002"] "Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] From 53c3cd9f8e91afc038b64b14cd63d1eb592f717a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:23:03 +0200 Subject: [PATCH 186/186] isort Tests --- Tests/bench_cffi_access.py | 1 + Tests/check_fli_overflow.py | 1 + Tests/check_imaging_leaks.py | 1 + Tests/check_j2k_leaks.py | 1 + Tests/check_j2k_overflow.py | 1 + Tests/check_jp2_overflow.py | 1 - Tests/check_jpeg_leaks.py | 1 + Tests/check_large_memory.py | 1 + Tests/check_large_memory_numpy.py | 1 + Tests/check_libtiff_segfault.py | 1 + Tests/check_png_dos.py | 1 + Tests/check_release_notes.py | 1 + Tests/check_wheel.py | 1 + Tests/conftest.py | 1 + Tests/createfontdatachunk.py | 1 + Tests/oss-fuzz/fuzzers.py | 1 + Tests/oss-fuzz/test_fuzzers.py | 1 + Tests/test_000_sanity.py | 1 + Tests/test_binary.py | 1 + Tests/test_bmp_reference.py | 1 + Tests/test_box_blur.py | 1 + Tests/test_color_lut.py | 1 + Tests/test_core_resources.py | 1 + Tests/test_decompression_bomb.py | 1 + Tests/test_deprecate.py | 1 + Tests/test_features.py | 1 + Tests/test_file_apng.py | 1 + Tests/test_file_blp.py | 1 + Tests/test_file_bmp.py | 1 + Tests/test_file_bufrstub.py | 1 + Tests/test_file_container.py | 1 + Tests/test_file_cur.py | 1 + Tests/test_file_dcx.py | 1 + Tests/test_file_dds.py | 1 + Tests/test_file_eps.py | 1 + Tests/test_file_fits.py | 1 + Tests/test_file_fli.py | 1 + Tests/test_file_fpx.py | 1 + Tests/test_file_ftex.py | 1 + Tests/test_file_gbr.py | 1 + Tests/test_file_gd.py | 1 + Tests/test_file_gif.py | 1 + Tests/test_file_gimpgradient.py | 1 + Tests/test_file_gimppalette.py | 1 + Tests/test_file_gribstub.py | 1 + Tests/test_file_hdf5stub.py | 1 + Tests/test_file_icns.py | 1 + Tests/test_file_ico.py | 1 + Tests/test_file_im.py | 1 + Tests/test_file_imt.py | 1 + Tests/test_file_iptc.py | 1 + Tests/test_file_jpeg.py | 1 + Tests/test_file_jpeg2k.py | 1 + Tests/test_file_libtiff.py | 1 + Tests/test_file_libtiff_small.py | 1 + Tests/test_file_mcidas.py | 1 + Tests/test_file_mic.py | 1 + Tests/test_file_mpo.py | 1 + Tests/test_file_msp.py | 1 + Tests/test_file_palm.py | 1 + Tests/test_file_pcd.py | 1 + Tests/test_file_pcx.py | 1 + Tests/test_file_pdf.py | 1 + Tests/test_file_pixar.py | 1 + Tests/test_file_png.py | 1 + Tests/test_file_ppm.py | 1 + Tests/test_file_psd.py | 1 + Tests/test_file_qoi.py | 1 + Tests/test_file_sgi.py | 1 + Tests/test_file_spider.py | 1 + Tests/test_file_sun.py | 1 + Tests/test_file_tar.py | 1 + Tests/test_file_tga.py | 1 + Tests/test_file_tiff.py | 1 + Tests/test_file_tiff_metadata.py | 1 + Tests/test_file_wal.py | 1 + Tests/test_file_webp.py | 1 + Tests/test_file_webp_alpha.py | 1 + Tests/test_file_webp_animated.py | 1 + Tests/test_file_webp_lossless.py | 1 + Tests/test_file_webp_metadata.py | 1 + Tests/test_file_wmf.py | 1 + Tests/test_file_xbm.py | 1 + Tests/test_file_xpm.py | 1 + Tests/test_file_xvthumb.py | 1 + Tests/test_font_bdf.py | 1 + Tests/test_font_crash.py | 1 + Tests/test_font_leaks.py | 1 + Tests/test_font_pcf.py | 1 + Tests/test_font_pcf_charsets.py | 1 + Tests/test_fontfile.py | 1 + Tests/test_format_hsv.py | 1 + Tests/test_format_lab.py | 1 + Tests/test_image.py | 1 + Tests/test_image_access.py | 1 + Tests/test_image_array.py | 1 + Tests/test_image_convert.py | 1 + Tests/test_image_copy.py | 1 + Tests/test_image_crop.py | 1 + Tests/test_image_draft.py | 1 + Tests/test_image_entropy.py | 1 + Tests/test_image_filter.py | 1 + Tests/test_image_frombytes.py | 1 + Tests/test_image_fromqimage.py | 1 + Tests/test_image_getbands.py | 1 + Tests/test_image_getbbox.py | 1 + Tests/test_image_getcolors.py | 1 + Tests/test_image_getdata.py | 1 + Tests/test_image_getextrema.py | 1 + Tests/test_image_getim.py | 1 + Tests/test_image_getpalette.py | 1 + Tests/test_image_getprojection.py | 1 + Tests/test_image_histogram.py | 1 + Tests/test_image_load.py | 1 + Tests/test_image_mode.py | 1 + Tests/test_image_paste.py | 1 + Tests/test_image_point.py | 1 + Tests/test_image_putalpha.py | 1 + Tests/test_image_putdata.py | 1 + Tests/test_image_putpalette.py | 1 + Tests/test_image_quantize.py | 1 + Tests/test_image_reduce.py | 1 + Tests/test_image_resample.py | 1 + Tests/test_image_resize.py | 1 + Tests/test_image_rotate.py | 1 + Tests/test_image_split.py | 1 + Tests/test_image_thumbnail.py | 1 + Tests/test_image_tobitmap.py | 1 + Tests/test_image_tobytes.py | 1 + Tests/test_image_transform.py | 1 + Tests/test_image_transpose.py | 1 + Tests/test_imagechops.py | 1 + Tests/test_imagecms.py | 1 + Tests/test_imagecolor.py | 1 + Tests/test_imagedraw.py | 1 + Tests/test_imagedraw2.py | 1 + Tests/test_imageenhance.py | 1 + Tests/test_imagefile.py | 1 + Tests/test_imagefont.py | 1 + Tests/test_imagefontctl.py | 1 + Tests/test_imagefontpil.py | 6 ++++-- Tests/test_imagegrab.py | 1 + Tests/test_imagemath.py | 1 + Tests/test_imagemorph.py | 1 + Tests/test_imageops.py | 1 + Tests/test_imageops_usm.py | 1 + Tests/test_imagepalette.py | 1 + Tests/test_imagepath.py | 1 + Tests/test_imageqt.py | 1 + Tests/test_imagesequence.py | 1 + Tests/test_imageshow.py | 1 + Tests/test_imagestat.py | 1 + Tests/test_imagetk.py | 1 + Tests/test_imagewin.py | 1 + Tests/test_imagewin_pointers.py | 1 + Tests/test_lib_image.py | 1 + Tests/test_lib_pack.py | 1 + Tests/test_locale.py | 1 + Tests/test_main.py | 1 + Tests/test_map.py | 1 + Tests/test_mode_i16.py | 1 + Tests/test_numpy.py | 1 + Tests/test_pdfparser.py | 1 + Tests/test_pickle.py | 1 + Tests/test_psdraw.py | 1 + Tests/test_pyroma.py | 1 + Tests/test_qt_image_qapplication.py | 1 + Tests/test_qt_image_toqimage.py | 1 + Tests/test_sgi_crash.py | 1 + Tests/test_shell_injection.py | 1 + Tests/test_tiff_ifdrational.py | 1 + Tests/test_uploader.py | 1 + Tests/test_util.py | 1 + Tests/test_webp_leaks.py | 1 + 174 files changed, 176 insertions(+), 3 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 8a37c7d51..ad15a9739 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,4 +1,5 @@ from __future__ import annotations + import time from PIL import PyAccess diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 0fabcb5d3..8cb6ac0a6 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image TEST_FILE = "Tests/images/fli_overflow.fli" diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 8c17c051d..eed326a4c 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 83a12e2c2..249c92cef 100644 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 982f6ea74..6f3aa437e 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py index 9afbff112..5adbb84b6 100755 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -14,7 +14,6 @@ # version. from __future__ import annotations - from PIL import Image repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 3cd37c7af..147deb285 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 9b83798d5..fb336f6e1 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 0ff3de8dc..d91d615f8 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index ee1d7d11f..17b533628 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 292fe4b7f..7b0ed2424 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,4 +1,5 @@ from __future__ import annotations + import zlib from io import BytesIO diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py index ebfaffa47..cf414d7ff 100644 --- a/Tests/check_release_notes.py +++ b/Tests/check_release_notes.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from pathlib import Path diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index afe4cc3ee..daf9e6211 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from PIL import features diff --git a/Tests/conftest.py b/Tests/conftest.py index cd64bd755..ac618c5b9 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 2e990b709..41c76f87e 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from __future__ import annotations + import base64 import os diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 3f3c1e388..5c00e9716 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import warnings diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a..bf4fb45ff 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -1,4 +1,5 @@ from __future__ import annotations + import subprocess import sys diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index c582dfad3..f64216bca 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 62da26636..41fb93fcf 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import _binary diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index bed8dc3a8..0da41e858 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import warnings diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index e798cba3d..461e6aaac 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 448ba2fac..fcd1169ef 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,4 +1,5 @@ from __future__ import annotations + from array import array import pytest diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 5275652f6..d3f76fdb1 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 391948d40..d3049eff1 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index d45a6603c..6c7f509a7 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import _deprecate diff --git a/Tests/test_features.py b/Tests/test_features.py index 8f0e4b418..b90c1d25f 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import re diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index e2c4569ce..23263b5d4 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageSequence, PngImagePlugin diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 4c1e38d1d..27ff7ab66 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 4cc92c5f6..225fb28ba 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 5780232a2..45081832e 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import BufrStubImagePlugin, Image diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 0da5d3824..95a5b2337 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ContainerIO, Image diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 08c3257f9..27b2bc914 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import CurImagePlugin, Image diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 25e4badbc..cba7c10bf 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index a605c8399..7064b74c0 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,5 +1,6 @@ """Test DdsImagePlugin""" from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 8b48e83ad..8def9a435 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 1383f9c5c..7444eb673 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 10bf36cc2..00377e0c9 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index af3b79815..d710070c0 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index a494c8029..0f9154e3d 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import FtexImagePlugin, Image diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 7dfe05396..d84004e14 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GbrImagePlugin, Image diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index ec80c54a1..e7db54fb4 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GdImageFile, UnidentifiedImageError diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 78b77e974..3e19940aa 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings from io import BytesIO diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index d5be46dc3..ceea1edd3 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import GimpGradientFile, ImagePalette diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 775d3b7cd..28855c28a 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL.GimpPaletteFile import GimpPaletteFile diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index d962e85a4..a4ce6dde6 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GribStubImagePlugin, Image diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 9c776b712..727644617 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Hdf5StubImagePlugin, Image diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index c62fffc5b..314fa8008 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import os import warnings diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index de9fa353a..99b3048d1 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import os diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 0cb26d06a..a031b3e88 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,4 +1,5 @@ from __future__ import annotations + import filecmp import warnings diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py index 3db488558..aa13d4407 100644 --- a/Tests/test_file_imt.py +++ b/Tests/test_file_imt.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index c44a08f52..a2c50ecef 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from io import BytesIO, StringIO diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 979c7e33d..232e51f91 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import re import warnings diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index aaa4104e5..94b02c9ff 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import re from io import BytesIO diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 65adf449d..494253c87 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,4 +1,5 @@ from __future__ import annotations + import base64 import io import itertools diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 9501c55a6..171e4a3f8 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index 4b31aaa78..73eba5cc8 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, McIdasImagePlugin diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index e7ea39ea9..8c43f7d7a 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index da62bc6d4..c7121ea28 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings from io import BytesIO diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f4e357ae0..9037ea33b 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 735840de4..eba694153 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os.path import subprocess diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 596a3414f..1a37c6ab3 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index f42ec4a68..2565e0b6d 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFile, PcxImagePlugin diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 9e07d9ed0..30c54c963 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import os import os.path diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index 63779f202..c6ddc54e7 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, PixarImagePlugin diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ff3862110..ae2a4772b 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,4 +1,5 @@ from __future__ import annotations + import re import sys import warnings diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index d8e259b1c..32de42ed4 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from io import BytesIO diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b1..16f049602 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index b7c945729..6dc468754 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, QoiImagePlugin diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 13698276b..bc45bbfd3 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, SgiImagePlugin diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index f21098754..42d833fb2 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,4 +1,5 @@ from __future__ import annotations + import tempfile import warnings from io import BytesIO diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 874b37b52..41f3b7d98 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 4470823cd..58226c330 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index d0f228573..eafb61d30 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os from glob import glob from itertools import product diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a50f50e5e..f0995679b 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import warnings from io import BytesIO diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index ee6968185..06689bc90 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import struct diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 0b84d0320..7acec9759 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import WalImageFile from .helper import assert_image_equal_tofile diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index c91818ef6..c49418ce3 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import re import sys diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 79d01a444..cfda35a09 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 22acb4be6..426fe7a02 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 6acf58ac3..08c80973a 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index a7b7bbcf6..deaf5e380 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 596dc8ba1..6e1d4c136 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, WmfImagePlugin diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index b086ffd68..69a0a1b38 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 265feab42..529a45580 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, XpmImagePlugin diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index 5848995c1..b87494eba 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, XVThumbImagePlugin diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 1e5eff2f1..e5e856186 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import BdfFontFile, FontFile diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index 388ee7118..e3c72c1ae 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageFont diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 6a038bb40..4e29a856b 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image, ImageDraw, ImageFont from .helper import PillowLeakTestCase, skip_unless_feature diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 4365b9310..e6abede07 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 950e5029f..4c2d7185e 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index ce1e02f63..eda8fb812 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import FontFile diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index fd47fae39..6395ae4aa 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,4 +1,5 @@ from __future__ import annotations + import colorsys import itertools diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index c7610ce8a..a55620e09 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_image.py b/Tests/test_image.py index 80f6583d8..dd989ad99 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import logging import os diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 4a794371d..4ae56fae0 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import subprocess import sys diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index b3e5d9e3e..0dacb3157 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 7c17040d3..d4ddc2a31 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index abf5f846f..3a26ef96e 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import copy import pytest diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 0bb54e5d8..5e02a3b0d 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 774272dd1..08c40af1f 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import fromstring, skip_unless_feature, tostring diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index 031fceda3..fce161224 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 5bd7ee0d2..3fa5dd242 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 017da499d..5d5e9f2df 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index b3ca43bde..76b576da5 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index e7701dbc4..64339e2cd 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index 9e792cfdf..b18a7202e 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index dea3a60a1..17460fa93 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index 873cc65bf..ace64279b 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index b17c8a786..6bbc4da9a 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index e969c8164..bc8a7485e 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index a5be972d3..4340f46f6 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index aa47be3b2..e90f5f505 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import hopper diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 7ba2f10b7..3ac6649e0 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 17847c4fd..36f8ba575 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,4 +1,5 @@ from __future__ import annotations + import logging import os diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index ad90d1250..3c1d494fa 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMode diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 0b87f6072..fd117f9db 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index fce45ec4f..2232b9442 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from .helper import assert_image_equal, hopper diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index 0ba7e5919..c44b048d5 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index d3cb13e2e..2648af8fa 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from array import array diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index de2d90242..43b65be2b 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 54c567aae..1475b027b 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index a4d0f5107..ba9100415 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMath, ImageMode diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 5a578dba5..af730dce1 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,4 +1,5 @@ from __future__ import annotations + from contextlib import contextmanager import pytest diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 0d3b43ee2..aedcf4a09 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -2,6 +2,7 @@ Tests for resize functionality. """ from __future__ import annotations + from itertools import permutations import pytest diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 0931aa32d..e63fef2c1 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 707508250..c39a100e7 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, features diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 9e6796ca2..7fa5692aa 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 156b9919d..89a41cf8e 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from .helper import assert_image_equal, fromstring, hopper diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index f6042bca5..8f15adac0 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import hopper diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index f5d5ab704..0fe9fd1d5 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,4 +1,5 @@ from __future__ import annotations + import math import pytest diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 66a2d9e29..01bf5a839 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL.Image import Transpose diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 8e3a738d7..2f0614385 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image, ImageChops from .helper import assert_image_equal, hopper diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 810394e6f..03332699a 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,4 +1,5 @@ from __future__ import annotations + import datetime import os import re diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index c0ffd2ebf..b602172b6 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageColor diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 379fe78cd..69aab4891 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,4 +1,5 @@ from __future__ import annotations + import contextlib import os.path diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index d729af14d..004c2d768 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os.path import pytest diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index f4e4d59be..e3d8a7ab2 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageEnhance diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 4804a554f..99731f352 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 807d581ed..d2c87d42a 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,4 +1,5 @@ from __future__ import annotations + import copy import os import re diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index bea532b05..09e68ea48 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageFont diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 9e0855101..be4be1c54 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,9 +1,11 @@ from __future__ import annotations + import struct -import pytest from io import BytesIO -from PIL import Image, ImageDraw, ImageFont, features, _util +import pytest + +from PIL import Image, ImageDraw, ImageFont, _util, features from .helper import assert_image_equal_tofile diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index b7683ec18..9d3d40398 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import shutil import subprocess diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 9281de6f6..622ad27ea 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMath diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 64a1785ea..0708ee639 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,5 +1,6 @@ # Test the ImageMorphology functionality from __future__ import annotations + import pytest from PIL import Image, ImageMorph, _imagingmorph diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 7980bead0..636b99dbe 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageOps, ImageStat, features diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 84d3a6950..8ffb9bff7 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e5b59b74a..be21464b4 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index ac3ea3281..5c6393e23 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,4 +1,5 @@ from __future__ import annotations + import array import math import struct diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 41d247f42..d55d980d9 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6d71e4d87..66d553bcb 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageSequence, TiffImagePlugin diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 761d28d30..0996ad41d 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageShow diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 7b56b89cc..01687db35 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageStat diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index bb20fbb6f..c06fc5823 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 6927eedcf..f93eabcb4 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageWin diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index bd154335a..63d6b903c 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image, ImageWin diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 92cad4ac1..1c642e4c9 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 1293f7628..e2024abbf 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 49b052fa4..db9557d7b 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,4 +1,5 @@ from __future__ import annotations + import locale import pytest diff --git a/Tests/test_main.py b/Tests/test_main.py index a84e61a7b..9f61a0c81 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import subprocess import sys diff --git a/Tests/test_map.py b/Tests/test_map.py index 76444f33d..9c79fe359 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 3e17d8dcc..d3ee511b7 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6f0e99b3f..24dff36a6 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index aeeafb6f1..a89d75b59 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -1,4 +1,5 @@ from __future__ import annotations + import time import pytest diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index eb687b57b..c445e3494 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pickle import pytest diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 77c7952e9..7f618d0f5 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import sys from io import BytesIO diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 08133b6c3..c2cea08ca 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import __version__ diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 49ca01677..ad2b5ad9b 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageQt diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 396bd9080..b26787ce6 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageQt diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index 37d72d451..dee6258ec 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index d93b03904..9f3e86a32 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,4 +1,5 @@ from __future__ import annotations + import shutil import pytest diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index e7b41fb47..c07e7f7d3 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,4 +1,5 @@ from __future__ import annotations + from fractions import Fraction from PIL import Image, TiffImagePlugin, features diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index 6b693f7cd..75326288f 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,4 +1,5 @@ from __future__ import annotations + from .helper import assert_image_equal, assert_image_similar, hopper diff --git a/Tests/test_util.py b/Tests/test_util.py index 4a312beb4..3395ef753 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import _util diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 28ebc7d79..0f51abc95 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image