From 0beb2228f9adb387a87c6b944cda2e427f5f264a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Oct 2024 12:44:25 +1100 Subject: [PATCH 001/175] Include JpegImageFile layers in state --- Tests/test_pickle.py | 11 +++++++++++ src/PIL/Image.py | 2 +- src/PIL/JpegImagePlugin.py | 7 +++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index d250ba36965..c4f8de013ec 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -74,6 +74,17 @@ def test_pickle_image( helper_pickle_file(tmp_path, protocol, test_file, test_mode) +def test_pickle_jpeg() -> None: + # Arrange + with Image.open("Tests/images/hopper.jpg") as image: + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) + + # Assert + assert len(unpickled_image.layer) == 3 + assert unpickled_image.layers == 3 + + def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = str(tmp_path / "temp.pkl") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c4d..ec5ce6cb1ee 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -763,7 +763,7 @@ def __getstate__(self) -> list[Any]: def __setstate__(self, state: list[Any]) -> None: Image.__init__(self) - info, mode, size, palette, data = state + info, mode, size, palette, data = state[:5] self.info = info self._mode = mode self._size = size diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e5e..7dec7521809 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -395,6 +395,13 @@ def __getattr__(self, name: str) -> Any: return getattr(self, "_" + name) raise AttributeError(name) + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.layers, self.layer] + + def __setstate__(self, state: list[Any]) -> None: + super().__setstate__(state) + self.layers, self.layer = state[5:] + def load_read(self, read_bytes: int) -> bytes: """ internal: read more image data From 98f975dbbe6ae0ef521522cdea6e00fdc81449c0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 18:56:23 +1100 Subject: [PATCH 002/175] Do not save XMP from info --- Tests/test_file_jpeg.py | 9 +++++++-- src/PIL/JpegImagePlugin.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cde951395a7..1b889eb40f4 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -998,8 +998,13 @@ def test_save_xmp(self, tmp_path: Path) -> None: with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"XMP test" - im.info["xmp"] = b"1" * 65504 - im.save(f) + # Check that XMP is not saved from image info + reloaded.save(f) + + with Image.open(f) as reloaded: + assert "xmp" not in reloaded.info + + im.save(f, xmp=b"1" * 65504) with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"1" * 65504 diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e5e..6937f2650d6 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -751,7 +751,7 @@ def validate_qtables( extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - xmp = info.get("xmp", im.info.get("xmp")) + xmp = info.get("xmp") if xmp: overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len From 203ca12626b22d185b59199b60ceb55081f6c3b2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Oct 2024 19:09:22 +1100 Subject: [PATCH 003/175] Allow encoderinfo to be set for appended images --- Tests/test_file_mpo.py | 12 ++++++++++++ src/PIL/Image.py | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index e0f42a26649..39aaa1613ce 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -293,3 +293,15 @@ def test_save_all() -> None: # Test that a single frame image will not be saved as an MPO jpg = roundtrip(im, save_all=True) assert "mp" not in jpg.info + + +def test_save_xmp() -> None: + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im2.encoderinfo = {"xmp": b"Second frame"} + im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) + + assert im_reloaded.info["xmp"] == b"First frame" + + im_reloaded.seek(1) + assert im_reloaded.info["xmp"] == b"Second frame" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c4d..586da7d3dba 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2565,7 +2565,7 @@ def save( self._ensure_mutable() save_all = params.pop("save_all", False) - self.encoderinfo = params + self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} self.encoderconfig: tuple[Any, ...] = () preinit() @@ -2612,6 +2612,11 @@ def save( except PermissionError: pass raise + finally: + try: + del self.encoderinfo + except AttributeError: + pass if open_fp: fp.close() From 8af2d7640ead3ddeba9fdc598dc8e72d580dfdfb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Oct 2024 23:26:13 +1100 Subject: [PATCH 004/175] Pass palette mode to putpalette --- Tests/test_file_gif.py | 6 ++++-- src/PIL/GifImagePlugin.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 16c8466f331..4eb8c0d02a7 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -4,6 +4,7 @@ from collections.abc import Generator from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -1431,7 +1432,8 @@ def test_saving_rgba(tmp_path: Path) -> None: assert reloaded_rgba.load()[0, 0][3] == 0 -def test_optimizing_p_rgba(tmp_path: Path) -> None: +@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) +def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: out = str(tmp_path / "temp.gif") im1 = Image.new("P", (100, 100)) @@ -1443,7 +1445,7 @@ def test_optimizing_p_rgba(tmp_path: Path) -> None: im2 = Image.new("P", (100, 100)) im2.putpalette(data, "RGBA") - im1.save(out, save_all=True, append_images=[im2]) + im1.save(out, save_all=True, append_images=[im2], **params) with Image.open(out) as reloaded: assert reloaded.n_frames == 2 diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index a7c4f8b2c57..47022d58436 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -695,8 +695,9 @@ def _write_multiple_frames( ) background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) - assert im_frames[0].im.palette is not None - background_im.putpalette(im_frames[0].im.palette) + first_palette = im_frames[0].im.palette + assert first_palette is not None + background_im.putpalette(first_palette, first_palette.mode) bbox = _getbbox(background_im, im_frame)[1] elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: From 0fe55d611505fefaaa0d4b7b3a87b803714762de Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 09:23:13 +0800 Subject: [PATCH 005/175] Isolate macOS build from Homebrew. --- .github/workflows/wheels-dependencies.sh | 98 +++++++++++++++++------- .github/workflows/wheels-test.sh | 15 ++-- .gitignore | 1 + pyproject.toml | 7 ++ setup.py | 90 +++++++++++++--------- wheels/multibuild | 2 +- 6 files changed, 139 insertions(+), 74 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 97c1adf098a..35b04c6d652 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -23,6 +23,8 @@ OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 +RAQM_VERSION=0.7.1 +FRIBIDI_VERSION=1.0.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.2.2 else @@ -38,7 +40,23 @@ BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 +function build_pkg_config { + if [ -e pkg-config-stamp ]; then return; fi + # This essentially duplicates the Homebrew recipe: + # /~https://github.com/Homebrew/homebrew-core/blob/master/Formula/p/pkg-config.rb + ORIGINAL_CFLAGS=$CFLAGS + CFLAGS="$CFLAGS -Wno-int-conversion" + build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ + --disable-debug --disable-host-tool --with-internal-glib \ + --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ + --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include + CFLAGS=$ORIGINAL_CFLAGS + export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config + touch pkg-config-stamp +} + function build_brotli { + if [ -e brotli-stamp ]; then return; fi local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack /~https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ @@ -48,25 +66,25 @@ function build_brotli { cp /usr/local/lib64/libbrotli* /usr/local/lib cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig fi + touch brotli-stamp } function build_harfbuzz { - python3 -m pip install meson ninja + if [ -e harfbuzz-stamp ]; then return; fi + python -m pip install meson ninja local out_dir=$(fetch_unpack /~https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) + && meson setup build --prefix=$BUILD_PREFIX --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ && meson install) if [[ "$MB_ML_LIBC" == "manylinux" ]]; then cp /usr/local/lib64/libharfbuzz* /usr/local/lib fi + touch harfbuzz-stamp } function build { - if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then - sudo chown -R runner /usr/local - fi build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel @@ -77,17 +95,23 @@ function build { if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig - fi else sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib build_libjpeg_turbo - build_tiff + if [ -n "$IS_MACOS" ]; then + # Custom tiff build to include jpeg; by default, configure won't include + # headers/libs in the custom macOS prefix + build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ + --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib + else + build_tiff + fi + build_libpng build_lcms2 build_openjpeg @@ -113,33 +137,55 @@ function build { fi build_harfbuzz + + if [ -n "$IS_MACOS" ]; then + build_simple fribidi $FRIBIDI_VERSION /~https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION tar.xz --enable-shared + build_simple raqm $RAQM_VERSION /~https://github.com/Host_Oman/libraqm/releases/download/v$RAQM_VERSION tar.gz --enable-shared + fi } +# Perform all dependency builds in the build subfolder. +mkdir -p build +pushd build > /dev/null + # Any stuff that you need to do before you start building the wheels # Runs in the root directory of this repository. -curl -fsSL -o pillow-depends-main.zip /~https://github.com/python-pillow/pillow-depends/archive/main.zip -untar pillow-depends-main.zip - -if [[ -n "$IS_MACOS" ]]; then - # libtiff and libxcb cause a conflict with building libtiff and libxcb - # libxau and libxdmcp cause an issue on macOS < 11 - # remove cairo to fix building harfbuzz on arm64 - # remove lcms2 and libpng to fix building openjpeg on arm64 - # remove jpeg-turbo to avoid inclusion on arm64 - # remove webp and zstd to avoid inclusion on x86_64 - # curl from brew requires zstd, use system curl - brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - brew remove --ignore-dependencies jpeg-turbo - else - brew remove --ignore-dependencies webp +if [[ ! -d pillow-depends-main ]]; then + if [[ ! -f pillow-depends-main.zip ]]; then + echo "Download pillow dependency sources..." + curl -fSL -o pillow-depends-main.zip /~https://github.com/python-pillow/pillow-depends/archive/main.zip fi + untar pillow-depends-main.zip +fi - brew install pkg-config +if [[ -n "$IS_MACOS" ]]; then + # Build and install into the `deps` folder. + BUILD_PREFIX=$(pwd)/deps + + # Homebrew (or similar packaging environments) install can contain some of + # the libraries that we're going to build. However, they may be compiled + # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, + # and they may bring in other dependencies that we don't want. The same will + # be true of any other locations on the path. To avoid conflicts, strip the + # path down to the bare mimimum (which, on macOS, won't include any + # development dependencies). + export PATH="$BUILD_PREFIX/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" + export CMAKE_PREFIX_PATH=$BUILD_PREFIX + + # Link the brew command into our isolated build directory. + mkdir -p "$BUILD_PREFIX/bin" + mkdir -p "$BUILD_PREFIX/lib" + + # Ensure pkg-confg and cmake are available + build_pkg_config + python -m pip install cmake fi wrap_wheel_builder build +# Return to the project root to finish the build +popd > /dev/null + # Append licenses for filename in wheels/dependency_licenses/*; do echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index b30b1725f94..05f5162c78a 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,16 +1,13 @@ #!/bin/bash set -e -if [[ "$OSTYPE" == "darwin"* ]]; then - brew install fribidi - export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" - if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then - sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib +# For Unix, Ensure fibidi is installed by the system. +if [[ "$OSTYPE" != "darwin"* ]]; then + if [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then + apk add curl fribidi + else + yum install -y fribidi fi -elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then - apk add curl fribidi -else - yum install -y fribidi fi python3 -m pip install numpy diff --git a/.gitignore b/.gitignore index 1dd6c917524..9b577325730 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ lib64/ parts/ sdist/ var/ +wheelhouse/ *.egg-info/ .installed.cfg *.egg diff --git a/pyproject.toml b/pyproject.toml index c55be769341..53546f0ab57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,10 +93,17 @@ version = { attr = "PIL.__version__" } [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 + config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" +# Add an explicit dependencies prefix for macOS, and don't request vendored libraries. +macos.config-settings = "raqm=enable imagequant=disable dependencies-prefix=./build/deps" + test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" +[tool.cibuildwheel.macos.environment] +PATH = "$(pwd)/build/deps/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" + [tool.black] exclude = "wheels/multibuild" diff --git a/setup.py b/setup.py index 60707083f6e..cb1da7c73f3 100644 --- a/setup.py +++ b/setup.py @@ -344,6 +344,11 @@ def __iter__(self) -> Iterator[str]: for x in ("raqm", "fribidi") ] + [ + ( + "dependencies-prefix", + None, + "The prefix where build dependencies are located.", + ), ("disable-platform-guessing", None, "Disable platform guessing on Linux"), ("debug", None, "Debug logging"), ] @@ -355,6 +360,7 @@ def check_configuration(option: str, value: str) -> bool | None: return True if value in configuration.get(option, []) else None def initialize_options(self) -> None: + self.dependencies_prefix = configuration.get("dependencies-prefix", []) self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) @@ -564,48 +570,56 @@ def build_extensions(self) -> None: ) elif sys.platform == "darwin": - # attempt to make sure we pick freetype2 over other versions - _add_directory(include_dirs, "/sw/include/freetype2") - _add_directory(include_dirs, "/sw/lib/freetype2/include") - # fink installation directories - _add_directory(library_dirs, "/sw/lib") - _add_directory(include_dirs, "/sw/include") - # darwin ports installation directories - _add_directory(library_dirs, "/opt/local/lib") - _add_directory(include_dirs, "/opt/local/include") - - # if Homebrew is installed, use its lib and include directories - try: - prefix = ( - subprocess.check_output(["brew", "--prefix"]) - .strip() - .decode("latin1") - ) - except Exception: - # Homebrew not installed - prefix = None + if self.dependencies_prefix: + # Use the explicitly provided prefixes for dependencies. + for prefix in self.dependencies_prefix: + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + else: + # Guess the dependency locations based on homebrew/fink/macports + # attempt to make sure we pick freetype2 over other versions + _add_directory(include_dirs, "/sw/include/freetype2") + _add_directory(include_dirs, "/sw/lib/freetype2/include") + # fink installation directories + _add_directory(library_dirs, "/sw/lib") + _add_directory(include_dirs, "/sw/include") + # darwin ports installation directories + _add_directory(library_dirs, "/opt/local/lib") + _add_directory(include_dirs, "/opt/local/include") + + # if Homebrew is installed, use its lib and include directories + try: + prefix = ( + subprocess.check_output(["brew", "--prefix"]) + .strip() + .decode("latin1") + ) + except Exception: + # Homebrew not installed + prefix = None - ft_prefix = None + ft_prefix = None - if prefix: - # add Homebrew's include and lib directories - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) - _add_directory( - include_dirs, os.path.join(prefix, "opt", "zlib", "include") - ) - ft_prefix = os.path.join(prefix, "opt", "freetype") + if prefix: + # add Homebrew's include and lib directories + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) + ft_prefix = os.path.join(prefix, "opt", "freetype") - if ft_prefix and os.path.isdir(ft_prefix): - # freetype might not be linked into Homebrew's prefix - _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) - _add_directory(include_dirs, os.path.join(ft_prefix, "include")) - else: - # fall back to freetype from XQuartz if - # Homebrew's freetype is missing - _add_directory(library_dirs, "/usr/X11/lib") - _add_directory(include_dirs, "/usr/X11/include") + if ft_prefix and os.path.isdir(ft_prefix): + # freetype might not be linked into Homebrew's prefix + _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) + _add_directory(include_dirs, os.path.join(ft_prefix, "include")) + else: + # fall back to freetype from XQuartz if + # Homebrew's freetype is missing + _add_directory(library_dirs, "/usr/X11/lib") + _add_directory(include_dirs, "/usr/X11/include") + # Add the macOS SDK path. sdk_path = self.get_macos_sdk_path() if sdk_path: _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) diff --git a/wheels/multibuild b/wheels/multibuild index 452dd2d1705..8a9333e307f 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 452dd2d1705f6b2375369a6570c415beb3163f70 +Subproject commit 8a9333e307fa3f24db2089a66f25e38a8272c261 From fc35fcc5ea844cd0184af6e999d0f35835a4254a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 10:55:30 +0800 Subject: [PATCH 006/175] Cleanups and typos identified by code review. --- .github/workflows/wheels-dependencies.sh | 8 ++++---- .github/workflows/wheels-test.sh | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 35b04c6d652..2c0835e7e56 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -23,7 +23,7 @@ OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -RAQM_VERSION=0.7.1 +RAQM_VERSION=0.10.2 FRIBIDI_VERSION=1.0.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.2.2 @@ -71,7 +71,7 @@ function build_brotli { function build_harfbuzz { if [ -e harfbuzz-stamp ]; then return; fi - python -m pip install meson ninja + python3 -m pip install meson ninja local out_dir=$(fetch_unpack /~https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ @@ -176,9 +176,9 @@ if [[ -n "$IS_MACOS" ]]; then mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" - # Ensure pkg-confg and cmake are available + # Ensure pkg-config and cmake are available build_pkg_config - python -m pip install cmake + python3 -m pip install cmake fi wrap_wheel_builder build diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 05f5162c78a..3a8fc87ba1e 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -# For Unix, Ensure fibidi is installed by the system. +# For Unix, ensure fribidi is installed by the system. if [[ "$OSTYPE" != "darwin"* ]]; then if [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi From 00809a25516e1396e3e3ee38a760733efffbd1a9 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 13:42:46 +0800 Subject: [PATCH 007/175] Tweaks to ensure isolation from Homebrew on x86_64. --- .github/workflows/wheels-dependencies.sh | 34 +++++++++++++++++------- pyproject.toml | 2 +- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2c0835e7e56..dbabd4aef28 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,6 +4,9 @@ if [ -z "$IS_MACOS" ]; then export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} export MB_ML_VER=${AUDITWHEEL_POLICY:9} + + # Build and install into the `build/deps` folder. + BUILD_PREFIX=$(pwd)/build/deps fi export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh @@ -84,6 +87,18 @@ function build_harfbuzz { touch harfbuzz-stamp } +function build_raqm { + if [ -e raqm-stamp ]; then return; fi + python3 -m pip install meson ninja + + local out_dir=$(fetch_unpack /~https://github.com/HOST-Oman/libraqm/releases/download/v$RAQM_VERSION/raqm-$RAQM_VERSION.tar.xz raqm-$RAQM_VERSION.tar.xz) + (cd $out_dir \ + && meson setup build --prefix=$BUILD_PREFIX) + (cd $out_dir/build \ + && meson install) + touch raqm-stamp +} + function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then @@ -105,9 +120,12 @@ function build { build_libjpeg_turbo if [ -n "$IS_MACOS" ]; then # Custom tiff build to include jpeg; by default, configure won't include - # headers/libs in the custom macOS prefix + # headers/libs in the custom macOS prefix. Explicitly disable webp and + # zstd, because on x86_64 macs, it will pick up the Homebrew versions of + # webp and zstd from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ - --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib + --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ + --disable-webp --disable-zstd else build_tiff fi @@ -140,7 +158,7 @@ function build { if [ -n "$IS_MACOS" ]; then build_simple fribidi $FRIBIDI_VERSION /~https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION tar.xz --enable-shared - build_simple raqm $RAQM_VERSION /~https://github.com/Host_Oman/libraqm/releases/download/v$RAQM_VERSION tar.gz --enable-shared + build_raqm fi } @@ -159,9 +177,6 @@ if [[ ! -d pillow-depends-main ]]; then fi if [[ -n "$IS_MACOS" ]]; then - # Build and install into the `deps` folder. - BUILD_PREFIX=$(pwd)/deps - # Homebrew (or similar packaging environments) install can contain some of # the libraries that we're going to build. However, they may be compiled # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, @@ -169,16 +184,17 @@ if [[ -n "$IS_MACOS" ]]; then # be true of any other locations on the path. To avoid conflicts, strip the # path down to the bare mimimum (which, on macOS, won't include any # development dependencies). - export PATH="$BUILD_PREFIX/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" + export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" export CMAKE_PREFIX_PATH=$BUILD_PREFIX # Link the brew command into our isolated build directory. mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" - # Ensure pkg-config and cmake are available + # Ensure pkg-config is available build_pkg_config - python3 -m pip install cmake + # Ensure cmake is available + python3 -m pip cmake fi wrap_wheel_builder build diff --git a/pyproject.toml b/pyproject.toml index 53546f0ab57..3839cf8402a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,7 @@ test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" [tool.cibuildwheel.macos.environment] -PATH = "$(pwd)/build/deps/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:$(dirname $(which python))" +PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" [tool.black] exclude = "wheels/multibuild" From 06dbfedb0fbfcf38f6d6298ac4979e82fb653956 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 13:51:37 +0800 Subject: [PATCH 008/175] Bump the multibuild version to fix jpeg-turbo issue. --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 8a9333e307f..22e8ad0c69a 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 8a9333e307fa3f24db2089a66f25e38a8272c261 +Subproject commit 22e8ad0c69acdb0c94abab74da6e09f2810d50d5 From 5a8373e527bbea93cb9c3be87c206683f488ad8e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 14:47:51 +0800 Subject: [PATCH 009/175] Correct a dumb pip invocation error. --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index dbabd4aef28..0990943d32a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -194,7 +194,7 @@ if [[ -n "$IS_MACOS" ]]; then # Ensure pkg-config is available build_pkg_config # Ensure cmake is available - python3 -m pip cmake + python3 -m pip install cmake fi wrap_wheel_builder build From 140a06e0fda60b7474dbdfde53b2ad2f1d344603 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 16:04:56 +0800 Subject: [PATCH 010/175] Explicitly disable libdeflate on libtiff. --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0990943d32a..c6f57200f40 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -125,7 +125,7 @@ function build { # webp and zstd from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ - --disable-webp --disable-zstd + --disable-webp --disable-zstd --disable-libdeflate else build_tiff fi From 0961d3d22bc966f19f68ce01db6d07373dfbe6bc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 16:10:29 +0800 Subject: [PATCH 011/175] Possible fix for linux build failures. --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index c6f57200f40..4d58bee901f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -113,7 +113,7 @@ function build { build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else - sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc + sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib From 43c34fc0a5c22b1c00152a246fd39fe02925043d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 25 Oct 2024 17:20:34 +0800 Subject: [PATCH 012/175] Copy manylinux lib64 files from the correct built prefix. --- .github/workflows/wheels-dependencies.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4d58bee901f..9dae287170a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -66,8 +66,8 @@ function build_brotli { && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libbrotli* /usr/local/lib - cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig + cp $BUILD_PREFIX/lib64/libbrotli* $BUILD_PREFIX/lib + cp $BUILD_PREFIX/lib64/pkgconfig/libbrotli* $BUILD_PREFIX/lib/pkgconfig fi touch brotli-stamp } @@ -82,7 +82,7 @@ function build_harfbuzz { (cd $out_dir/build \ && meson install) if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libharfbuzz* /usr/local/lib + cp $BUILD_PREFIX/lib64/libharfbuzz* $BUILD_PREFIX/lib fi touch harfbuzz-stamp } @@ -113,7 +113,7 @@ function build { build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else - sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc + sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib @@ -133,8 +133,8 @@ function build { build_libpng build_lcms2 build_openjpeg - if [ -f /usr/local/lib64/libopenjp2.so ]; then - cp /usr/local/lib64/libopenjp2.so /usr/local/lib + if [ -f $BUILD_PREFIX/lib64/libopenjp2.so ]; then + cp $BUILD_PREFIX/lib64/libopenjp2.so $BUILD_PREFIX/lib fi ORIGINAL_CFLAGS=$CFLAGS From 4b9399f8bfb08d4ed1a1113ed6fd4b6137f7c2aa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Oct 2024 22:00:45 +1100 Subject: [PATCH 013/175] Use register_handler --- Tests/test_file_bufrstub.py | 2 +- Tests/test_file_gribstub.py | 2 +- Tests/test_file_hdf5stub.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 77ee5b0ea12..fc8920317c5 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -83,4 +83,4 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: im.save(temp_file) assert handler.saved - BufrStubImagePlugin._handler = None + BufrStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index aba473d24d0..02e464ff190 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -83,4 +83,4 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: im.save(temp_file) assert handler.saved - GribStubImagePlugin._handler = None + GribStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 8275bd0d890..024be9e80cf 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -85,4 +85,4 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: im.save(temp_file) assert handler.saved - Hdf5StubImagePlugin._handler = None + Hdf5StubImagePlugin.register_handler(None) From 3e0849bfb56f6fa8c73ff1bde044551ed72ac2d3 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 25 Oct 2024 20:35:17 +0200 Subject: [PATCH 014/175] winbuild: Replace zlib with zlib-ng built with CMake --- winbuild/build_prepare.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a21fbef9148..2065e41a96e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -120,11 +120,10 @@ def cmd_msbuild( "OPENJPEG": "2.5.2", "TIFF": "4.6.0", "XZ": "5.6.3", - "ZLIB": "1.3.1", + "ZLIBNG": "2.2.2", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) -V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation @@ -161,18 +160,22 @@ def cmd_msbuild( "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": f"https://zlib.net/zlib{V['ZLIB_DOTLESS']}.zip", - "filename": f"zlib{V['ZLIB_DOTLESS']}.zip", - "dir": f"zlib-{V['ZLIB']}", - "license": "README", - "license_pattern": "Copyright notice:\n\n(.+)$", + "url": f"/~https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.zip", + "filename": f"zlib-ng-{V['ZLIBNG']}.zip", + "dir": f"zlib-ng-{V['ZLIBNG']}", + "license": "LICENSE.md", + "patch": { + r"CMakeLists.txt": { + "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlib)", # noqa: E501 + }, + }, "build": [ - cmd_nmake(r"win32\Makefile.msc", "clean"), - cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), - cmd_copy("zlib.lib", "z.lib"), + *cmds_cmake( + "zlib", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON" + ), ], "headers": [r"z*.h"], - "libs": [r"*.lib"], + "libs": [r"zlib.lib"], }, "xz": { "url": f"/~https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz", From 7885066e5fc6ae56db0bef18eb2c188a6aa9a181 Mon Sep 17 00:00:00 2001 From: Nulano Date: Fri, 25 Oct 2024 20:37:21 +0200 Subject: [PATCH 015/175] PIL.features: Add a compile-time zlib-ng feature flag and version number --- Tests/check_wheel.py | 3 +++ docs/reference/features.rst | 1 + src/PIL/features.py | 7 ++++++- src/_imaging.c | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f58a..374e48e8ad3 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -34,10 +34,13 @@ def test_wheel_features() -> None: "fribidi", "harfbuzz", "libjpeg_turbo", + "zlib_ng", "xcb", } if sys.platform == "win32": expected_features.remove("xcb") + else: + expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features diff --git a/docs/reference/features.rst b/docs/reference/features.rst index fcff9673567..427c0f606d2 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,6 +54,7 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. +* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. diff --git a/src/PIL/features.py b/src/PIL/features.py index 75d59e01c40..3645e3defc4 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -127,6 +127,7 @@ def get_supported_codecs() -> list[str]: "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), "xcb": ("PIL._imaging", "HAVE_XCB", None), } @@ -308,7 +309,11 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: # this check is also in src/_imagingcms.c:setup_module() version_static = tuple(int(x) for x in v.split(".")) < (2, 7) t = "compiled for" if version_static else "loaded" - if name == "raqm": + if name == "zlib": + zlib_ng_version = version_feature("zlib_ng") + if zlib_ng_version is not None: + v += ", compiled for zlib-ng " + zlib_ng_version + elif name == "raqm": for f in ("fribidi", "harfbuzz"): v2 = version_feature(f) if v2 is not None: diff --git a/src/_imaging.c b/src/_imaging.c index 2db4486b23e..5d6d97bedab 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4397,6 +4397,20 @@ setup_module(PyObject *m) { } #endif + PyObject *have_zlibng; +#ifdef ZLIBNG_VERSION + have_zlibng = Py_True; + { + PyObject *v = PyUnicode_FromString(ZLIBNG_VERSION); + PyDict_SetItemString(d, "zlib_ng_version", v ? v : Py_None); + Py_XDECREF(v); + } +#else + have_zlibng = Py_False; +#endif + Py_INCREF(have_zlibng); + PyModule_AddObject(m, "HAVE_ZLIBNG", have_zlibng); + #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); From ed910a68d637661ccb4c4a7c1245769176fec93d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Oct 2024 11:42:35 +1100 Subject: [PATCH 016/175] Only replace version suffix if zlib-ng is present --- Tests/test_features.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index ed792997376..f8f7f6eec59 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -36,10 +36,11 @@ def test(name: str, function: Callable[[str], str | None]) -> None: else: assert function(name) == version if name != "PIL": - if name == "zlib" and version is not None: - version = re.sub(".zlib-ng$", "", version) - elif name == "libtiff" and version is not None: - version = re.sub("t$", "", version) + if version is not None: + if name == "zlib" and features.check_feature("zlib_ng"): + version = re.sub(".zlib-ng$", "", version) + elif name == "libtiff": + version = re.sub("t$", "", version) assert version is None or re.search(r"\d+(\.\d+)*$", version) for module in features.modules: From 08554684b3f382f3d729db5e6cd1fe94126dab66 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 07:46:59 +0800 Subject: [PATCH 017/175] Revert fribidi/raqm changes for macOS builds. --- .github/workflows/wheels-dependencies.sh | 5 ----- .github/workflows/wheels-test.sh | 21 ++++++++++++++++----- pyproject.toml | 4 ++-- setup.py | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 9dae287170a..22084237118 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -155,11 +155,6 @@ function build { fi build_harfbuzz - - if [ -n "$IS_MACOS" ]; then - build_simple fribidi $FRIBIDI_VERSION /~https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION tar.xz --enable-shared - build_raqm - fi } # Perform all dependency builds in the build subfolder. diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 3a8fc87ba1e..e671c8d45a8 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,13 +1,24 @@ #!/bin/bash set -e -# For Unix, ensure fribidi is installed by the system. -if [[ "$OSTYPE" != "darwin"* ]]; then - if [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then - apk add curl fribidi +# Ensure fribidi is installed by the system. +if [[ "$OSTYPE" == "darwin"* ]]; then + # If Homebrew is on the path during the build, it may leak into the wheels. + # However, we need a *do* need Homebrew to provide a copy of fribidi for + # testing purposes so that we can verify the fribidi shim works as expected. + if [[ "$(uname -m)" == "x86_64" ]]; then + HOMEBREW_HOME=/usr/local/homebrew else - yum install -y fribidi + HOMEBREW_HOME=/opt/homebrew fi + $HOMEBREW_HOME/bin/brew install fribidi + + # Add the Homebrew lib folder so that vendored libraries can be found. + export DYLD_LIBRARY_PATH=$HOMEBREW_HOME/lib +elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then + apk add curl fribidi +else + yum install -y fribidi fi python3 -m pip install numpy diff --git a/pyproject.toml b/pyproject.toml index 3839cf8402a..da8548dcbd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,8 +95,8 @@ before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" -# Add an explicit dependencies prefix for macOS, and don't request vendored libraries. -macos.config-settings = "raqm=enable imagequant=disable dependencies-prefix=./build/deps" +# Add an explicit dependencies prefix for macOS. +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index 16c25cbda80..38d793b1412 100644 --- a/setup.py +++ b/setup.py @@ -454,7 +454,7 @@ def _remove_extension(self, name: str) -> None: def get_macos_sdk_path(self) -> str | None: try: sdk_path = ( - subprocess.check_output(["xcrun", "--show-sdk-path"]) + subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", "macosx"]) .strip() .decode("latin1") ) From 8308bf361e7d4bac39b1710e4c3c0725c7ad07ac Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 07:47:30 +0800 Subject: [PATCH 018/175] Bump multibuild to include more cmake changes. --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 22e8ad0c69a..9a9d1275f02 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 22e8ad0c69acdb0c94abab74da6e09f2810d50d5 +Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 From c74a5bdd85c3314fbdbbe2a72d17adad85fae5f0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:02:05 +0800 Subject: [PATCH 019/175] Correct paths used for Linux build. --- .github/workflows/wheels-dependencies.sh | 31 ++++-------------------- pyproject.toml | 4 +++ setup.py | 3 +-- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 22084237118..0eb15d55315 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -26,8 +26,6 @@ OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -RAQM_VERSION=0.10.2 -FRIBIDI_VERSION=1.0.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.2.2 else @@ -63,12 +61,8 @@ function build_brotli { local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack /~https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ - && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp $BUILD_PREFIX/lib64/libbrotli* $BUILD_PREFIX/lib - cp $BUILD_PREFIX/lib64/pkgconfig/libbrotli* $BUILD_PREFIX/lib/pkgconfig - fi touch brotli-stamp } @@ -78,27 +72,12 @@ function build_harfbuzz { local out_dir=$(fetch_unpack /~https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX --buildtype=release -Dfreetype=enabled -Dglib=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ && meson install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp $BUILD_PREFIX/lib64/libharfbuzz* $BUILD_PREFIX/lib - fi touch harfbuzz-stamp } -function build_raqm { - if [ -e raqm-stamp ]; then return; fi - python3 -m pip install meson ninja - - local out_dir=$(fetch_unpack /~https://github.com/HOST-Oman/libraqm/releases/download/v$RAQM_VERSION/raqm-$RAQM_VERSION.tar.xz raqm-$RAQM_VERSION.tar.xz) - (cd $out_dir \ - && meson setup build --prefix=$BUILD_PREFIX) - (cd $out_dir/build \ - && meson install) - touch raqm-stamp -} - function build { build_xz if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then @@ -133,9 +112,6 @@ function build { build_libpng build_lcms2 build_openjpeg - if [ -f $BUILD_PREFIX/lib64/libopenjp2.so ]; then - cp $BUILD_PREFIX/lib64/libopenjp2.so $BUILD_PREFIX/lib - fi ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" @@ -190,6 +166,9 @@ if [[ -n "$IS_MACOS" ]]; then build_pkg_config # Ensure cmake is available python3 -m pip install cmake +else + # Ensure that any built libraries are on the linker path. + export LD_LIBRARY_PATH=$BUILD_PREFIX/lib fi wrap_wheel_builder build diff --git a/pyproject.toml b/pyproject.toml index da8548dcbd3..af3d0475750 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,10 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +[tool.cibuildwheel.linux.environment] +LD_LIBRARY_PATH = "$(pwd)/build/deps/lib" +PKG_CONFIG_PATH = "$(pwd)/build/deps/lib/pkgconfig:$(pwd)/build/deps/share/pkgconfig" + [tool.black] exclude = "wheels/multibuild" diff --git a/setup.py b/setup.py index 38d793b1412..d7e305fc0e1 100644 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ def get_version() -> str: "codec_fd", ) -DEBUG = False +DEBUG = True class DependencyException(Exception): @@ -479,7 +479,6 @@ def build_extensions(self) -> None: pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): pkg_config = _pkg_config - # # add configured kits for root_name, lib_name in { From 72d81e24a81e6e765f21e5794218f080e7718ec7 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:25:47 +0800 Subject: [PATCH 020/175] Simplify Linux config by correcting a logic error in macOS config. --- .github/workflows/wheels-dependencies.sh | 7 ++----- pyproject.toml | 4 ---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0eb15d55315..2e915364abb 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,8 +4,8 @@ if [ -z "$IS_MACOS" ]; then export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} export MB_ML_VER=${AUDITWHEEL_POLICY:9} - - # Build and install into the `build/deps` folder. +else + # Build and install macOS builds into the `build/deps` folder. BUILD_PREFIX=$(pwd)/build/deps fi export PLAT=$CIBW_ARCHS @@ -166,9 +166,6 @@ if [[ -n "$IS_MACOS" ]]; then build_pkg_config # Ensure cmake is available python3 -m pip install cmake -else - # Ensure that any built libraries are on the linker path. - export LD_LIBRARY_PATH=$BUILD_PREFIX/lib fi wrap_wheel_builder build diff --git a/pyproject.toml b/pyproject.toml index af3d0475750..da8548dcbd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,10 +104,6 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" -[tool.cibuildwheel.linux.environment] -LD_LIBRARY_PATH = "$(pwd)/build/deps/lib" -PKG_CONFIG_PATH = "$(pwd)/build/deps/lib/pkgconfig:$(pwd)/build/deps/share/pkgconfig" - [tool.black] exclude = "wheels/multibuild" From ec214e4d7caa471c087e0d690b20d75b6dd929fc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:39:58 +0800 Subject: [PATCH 021/175] Can't check IS_MACOS until common_utils is invoked. --- .github/workflows/wheels-dependencies.sh | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2e915364abb..9ce851f0f93 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,19 +1,21 @@ #!/bin/bash # Define custom utilities +export PLAT=$CIBW_ARCHS +source wheels/multibuild/common_utils.sh +source wheels/multibuild/library_builders.sh +if [ -z "$IS_MACOS" ]; then + source wheels/multibuild/manylinux_utils.sh +fi + # Test for macOS with [ -n "$IS_MACOS" ] +echo "IS MACOS: $IS_MACOS" if [ -z "$IS_MACOS" ]; then export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} export MB_ML_VER=${AUDITWHEEL_POLICY:9} -else +elseac # Build and install macOS builds into the `build/deps` folder. BUILD_PREFIX=$(pwd)/build/deps fi -export PLAT=$CIBW_ARCHS -source wheels/multibuild/common_utils.sh -source wheels/multibuild/library_builders.sh -if [ -z "$IS_MACOS" ]; then - source wheels/multibuild/manylinux_utils.sh -fi ARCHIVE_SDIR=pillow-depends-main From d1a4f8029b2d89f19e4652c96e3b1909b90aa5df Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 09:54:15 +0800 Subject: [PATCH 022/175] Don't use multibuild variables before invoking multibuild. --- .github/workflows/wheels-dependencies.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 9ce851f0f93..fd13930db46 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,4 +1,14 @@ #!/bin/bash + +# Setup that needs to be done before multibuild utils are invoked +if [[ "$(uname -s)" == "Darwin" ]]; then + # Build and install macOS builds into the `build/deps` folder. + BUILD_PREFIX=$(pwd)/build/deps +else + export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + export MB_ML_VER=${AUDITWHEEL_POLICY:9} +fi + # Define custom utilities export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh @@ -7,16 +17,6 @@ if [ -z "$IS_MACOS" ]; then source wheels/multibuild/manylinux_utils.sh fi -# Test for macOS with [ -n "$IS_MACOS" ] -echo "IS MACOS: $IS_MACOS" -if [ -z "$IS_MACOS" ]; then - export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} - export MB_ML_VER=${AUDITWHEEL_POLICY:9} -elseac - # Build and install macOS builds into the `build/deps` folder. - BUILD_PREFIX=$(pwd)/build/deps -fi - ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds From 6d137043f712d6790eba835a8ec727e0a4fbb603 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 29 Oct 2024 10:01:46 +0800 Subject: [PATCH 023/175] Remove stray debug. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d7e305fc0e1..38d793b1412 100644 --- a/setup.py +++ b/setup.py @@ -131,7 +131,7 @@ def get_version() -> str: "codec_fd", ) -DEBUG = True +DEBUG = False class DependencyException(Exception): @@ -479,6 +479,7 @@ def build_extensions(self) -> None: pkg_config = None if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")): pkg_config = _pkg_config + # # add configured kits for root_name, lib_name in { From c6912f81ff20211c2638be64bc198024aa9a5b40 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 30 Oct 2024 05:43:23 +0800 Subject: [PATCH 024/175] Corrected typo in code comment. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index e671c8d45a8..9bcf42458cc 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -4,7 +4,7 @@ set -e # Ensure fribidi is installed by the system. if [[ "$OSTYPE" == "darwin"* ]]; then # If Homebrew is on the path during the build, it may leak into the wheels. - # However, we need a *do* need Homebrew to provide a copy of fribidi for + # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then HOMEBREW_HOME=/usr/local/homebrew From 01270b5859800355b12fa4403c7684d9bf72c6f5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 31 Oct 2024 06:27:54 +0800 Subject: [PATCH 025/175] Use the intended entry point for the x86_64 brew binary. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 9bcf42458cc..e2ef337119e 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -7,7 +7,7 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then - HOMEBREW_HOME=/usr/local/homebrew + HOMEBREW_HOME=/usr/local else HOMEBREW_HOME=/opt/homebrew fi From 51e36230660667df38c68379314b652b34da47e5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 31 Oct 2024 09:50:38 +0800 Subject: [PATCH 026/175] Revert x86_64 homebrew location change (with explanation). --- .github/workflows/wheels-test.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index e2ef337119e..b697b740ec0 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -7,7 +7,10 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then - HOMEBREW_HOME=/usr/local + # Use the "installed" location, rather than /usr/local, for two reasons: + # firstly, Homebrew allows libraries to be *installed*, but not linked; + # and secondly, we don't want any *other* leakage from /usr/local. + HOMEBREW_HOME=/usr/local/Homebrew else HOMEBREW_HOME=/opt/homebrew fi From aae90c3c016ee36b838d1ca5d522301d98731261 Mon Sep 17 00:00:00 2001 From: "Pietro F. Fontana" Date: Fri, 1 Nov 2024 15:42:08 +0100 Subject: [PATCH 027/175] Allow linking to shared zlib If compiled as shared library zlib produces `zlib1.dll` and `zdll.lib`. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index def3417845d..48863c3974e 100644 --- a/setup.py +++ b/setup.py @@ -690,6 +690,8 @@ def build_extensions(self) -> None: feature.set("zlib", "z") elif sys.platform == "win32" and _find_library_file(self, "zlib"): feature.set("zlib", "zlib") # alternative name + elif sys.platform == "win32" and _find_library_file(self, "zdll"): + feature.set("zlib", "zdll") # different name if shared if feature.want("jpeg"): _dbg("Looking for jpeg") From 37679c8673e44f31be060ff7bc6772c7f178bee7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 4 Nov 2024 20:55:00 +1100 Subject: [PATCH 028/175] Pass IFDs to libtiff as TIFF_LONG8 --- src/PIL/TiffImagePlugin.py | 4 +++- src/encode.c | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a5f..c95b65b8ea2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1915,7 +1915,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if not getattr(Image.core, "libtiff_support_custom_tags", False): continue - if tag in ifd.tagtype: + if tag in TiffTags.TAGS_V2_GROUPS: + types[tag] = TiffTags.LONG8 + elif tag in ifd.tagtype: types[tag] = ifd.tagtype[tag] elif not (isinstance(value, (int, float, str, bytes))): continue diff --git a/src/encode.c b/src/encode.c index 1a4cd489da2..09a86a7151c 100644 --- a/src/encode.c +++ b/src/encode.c @@ -736,7 +736,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { } if (tag_type) { int type_int = PyLong_AsLong(tag_type); - if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { + if (type_int >= TIFF_BYTE && type_int <= TIFF_LONG8) { type = (TIFFDataType)type_int; } } @@ -929,7 +929,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value) ); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField( @@ -959,6 +959,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) ); + } else if (type == TIFF_LONG8) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + ); } else { TRACE( ("Unhandled type for key %d : %s \n", From b6413cd58818c6518ede6638a391bac15d812db0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 07:16:49 +1100 Subject: [PATCH 029/175] Cast to uint64_t --- src/encode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encode.c b/src/encode.c index 09a86a7151c..d369a1b4598 100644 --- a/src/encode.c +++ b/src/encode.c @@ -961,7 +961,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ); } else if (type == TIFF_LONG8) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + &encoder->state, (ttag_t)key_int, (uint64_t)PyLong_AsLongLong(value) ); } else { TRACE( From 3cdaee45f50a1a4c020c382b516299a2bb531ec7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 20:15:23 +1100 Subject: [PATCH 030/175] Raise UnidentifiedImageError when opening TIFF without dimensions --- Tests/test_imagefile.py | 13 +++++++++++++ src/PIL/TiffImagePlugin.py | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 1ee68492630..8bef90ce43c 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -93,6 +93,19 @@ def test_ico(self) -> None: assert p.image is not None assert (48, 48) == p.image.size + @pytest.mark.filterwarnings("ignore:Corrupt EXIF data") + def test_incremental_tiff(self) -> None: + with ImageFile.Parser() as p: + with open("Tests/images/hopper.tif", "rb") as f: + p.feed(f.read(1024)) + + # Check that insufficient data was given in the first feed + assert not p.image + + p.feed(f.read()) + assert p.image is not None + assert (128, 128) == p.image.size + @skip_unless_feature("webp") def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a5f..d4dcab2863a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1433,8 +1433,12 @@ def _setup(self) -> None: logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size - xsize = self.tag_v2.get(IMAGEWIDTH) - ysize = self.tag_v2.get(IMAGELENGTH) + try: + xsize = self.tag_v2[IMAGEWIDTH] + ysize = self.tag_v2[IMAGELENGTH] + except KeyError as e: + msg = "Missing dimensions" + raise TypeError(msg) from e if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) From a1d862f1d67e621213e0239d3e45838abeab363e Mon Sep 17 00:00:00 2001 From: "Pietro F. Fontana" Date: Tue, 5 Nov 2024 15:39:03 +0100 Subject: [PATCH 031/175] Be more specific in comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 48863c3974e..68252f3a98b 100644 --- a/setup.py +++ b/setup.py @@ -691,7 +691,7 @@ def build_extensions(self) -> None: elif sys.platform == "win32" and _find_library_file(self, "zlib"): feature.set("zlib", "zlib") # alternative name elif sys.platform == "win32" and _find_library_file(self, "zdll"): - feature.set("zlib", "zdll") # different name if shared + feature.set("zlib", "zdll") # dll import library if feature.want("jpeg"): _dbg("Looking for jpeg") From e82b5398e750c5dd37dfcc2edaf6b947371b53ef Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 6 Nov 2024 14:12:07 +0800 Subject: [PATCH 032/175] Correct handling of vendored fribidi. --- .github/workflows/wheels-dependencies.sh | 46 ++++++++++++++++-------- .github/workflows/wheels-test.sh | 8 +++-- .gitignore | 4 +++ pyproject.toml | 4 +-- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index fd13930db46..ff06e99d276 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,16 +1,34 @@ #!/bin/bash # Setup that needs to be done before multibuild utils are invoked +PROJECTDIR=$(pwd) if [[ "$(uname -s)" == "Darwin" ]]; then - # Build and install macOS builds into the `build/deps` folder. - BUILD_PREFIX=$(pwd)/build/deps + # Safety check - macOS builds require that CIBW_ARCHS is set, and that it + # only contains a single value (even though cwbuildwheel) allows multiple + # values in CIBW_ARCHS. + if [[ -z "$CIBW_ARCHS" ]]; then + echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." + exit 1 + fi + if [[ "$CIBW_ARCHS" == *" "* ]]; then + echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS." + exit 1 + fi + + # Build macOS dependencies in `build/darwin` + # Install them into `build/deps/darwin` + WORKDIR=$(pwd)/build/darwin + BUILD_PREFIX=$(pwd)/build/deps/darwin + PLAT=$CIBW_ARCHS else - export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} - export MB_ML_VER=${AUDITWHEEL_POLICY:9} + # Build prefix will default to /usr/local + WORKDIR=$(pwd)/build + PLAT=$CIBW_ARCHS + MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + MB_ML_VER=${AUDITWHEEL_POLICY:9} fi # Define custom utilities -export PLAT=$CIBW_ARCHS source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh if [ -z "$IS_MACOS" ]; then @@ -117,9 +135,6 @@ function build { ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" - if [[ -n "$IS_MACOS" ]]; then - CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" - fi build_libwebp CFLAGS=$ORIGINAL_CFLAGS @@ -127,7 +142,7 @@ function build { if [ -n "$IS_MACOS" ]; then # Custom freetype build - build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no + build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --without-harfbuzz else build_freetype fi @@ -136,17 +151,18 @@ function build { } # Perform all dependency builds in the build subfolder. -mkdir -p build -pushd build > /dev/null +mkdir -p $WORKDIR +pushd $WORKDIR > /dev/null # Any stuff that you need to do before you start building the wheels # Runs in the root directory of this repository. -if [[ ! -d pillow-depends-main ]]; then - if [[ ! -f pillow-depends-main.zip ]]; then +if [[ ! -d $WORKDIR/pillow-depends-main ]]; then + if [[ ! -f $PROJECTDIR/pillow-depends-main.zip ]]; then echo "Download pillow dependency sources..." - curl -fSL -o pillow-depends-main.zip /~https://github.com/python-pillow/pillow-depends/archive/main.zip + curl -fSL -o $PROJECTDIR/pillow-depends-main.zip /~https://github.com/python-pillow/pillow-depends/archive/main.zip fi - untar pillow-depends-main.zip + echo "Unpacking pillow dependency sources..." + untar $PROJECTDIR/pillow-depends-main.zip fi if [[ -n "$IS_MACOS" ]]; then diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index b697b740ec0..d1db8e8a5ec 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -16,8 +16,12 @@ if [[ "$OSTYPE" == "darwin"* ]]; then fi $HOMEBREW_HOME/bin/brew install fribidi - # Add the Homebrew lib folder so that vendored libraries can be found. - export DYLD_LIBRARY_PATH=$HOMEBREW_HOME/lib + # Add the lib folder for fribidi so that the vendored library can be found. + # Don't use /opt/homebrew/lib directly - use the lib folder where the + # installed copy of fribidi is cellared. This ensures we don't pick up the + # Homebrew version of any other library that we're dependent on (most notably, + # freetype). + export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_HOME/lib/libfribidi.dylib)) elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi else diff --git a/.gitignore b/.gitignore index 9b577325730..3033c2ea7ae 100644 --- a/.gitignore +++ b/.gitignore @@ -91,5 +91,9 @@ Tests/images/msp Tests/images/picins Tests/images/sunraster +# Test and dependency downloads +pillow-depends-main.zip +pillow-test-images.zip + # pyinstaller *.spec diff --git a/pyproject.toml b/pyproject.toml index da8548dcbd3..26fa4581706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,13 +96,13 @@ build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" # Add an explicit dependencies prefix for macOS. -macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps" +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps/darwin" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" [tool.cibuildwheel.macos.environment] -PATH = "$(pwd)/build/deps/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" [tool.black] exclude = "wheels/multibuild" From 4e35852ccc97f3e455f5130f1cd22f2f10292955 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 8 Nov 2024 05:34:35 +0800 Subject: [PATCH 033/175] Correct typo in comment. Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index ff06e99d276..2dea8c62a95 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,7 +4,7 @@ PROJECTDIR=$(pwd) if [[ "$(uname -s)" == "Darwin" ]]; then # Safety check - macOS builds require that CIBW_ARCHS is set, and that it - # only contains a single value (even though cwbuildwheel) allows multiple + # only contains a single value (even though cibuildwheel) allows multiple # values in CIBW_ARCHS. if [[ -z "$CIBW_ARCHS" ]]; then echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." From 0a5b0f29e58cf9763b886e3ce300a79b5f4bac63 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 00:18:59 +1100 Subject: [PATCH 034/175] Run gcc problem matcher on Python 3.13 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87acd7ddbc0..c33309bbf03 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,7 +116,7 @@ jobs: GHA_PYTHON_VERSION: ${{ matrix.python-version }} - name: Register gcc problem matcher - if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'" run: echo "::add-matcher::.github/problem-matchers/gcc.json" - name: Build From 681a03b1c38b26a36cddda812ead882b4af24eb4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 9 Nov 2024 10:29:29 +0800 Subject: [PATCH 035/175] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels-dependencies.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 2dea8c62a95..f4106429d46 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -4,8 +4,8 @@ PROJECTDIR=$(pwd) if [[ "$(uname -s)" == "Darwin" ]]; then # Safety check - macOS builds require that CIBW_ARCHS is set, and that it - # only contains a single value (even though cibuildwheel) allows multiple - # values in CIBW_ARCHS. + # only contains a single value (even though cibuildwheel allows multiple + # values in CIBW_ARCHS). if [[ -z "$CIBW_ARCHS" ]]; then echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." exit 1 @@ -171,7 +171,7 @@ if [[ -n "$IS_MACOS" ]]; then # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, # and they may bring in other dependencies that we don't want. The same will # be true of any other locations on the path. To avoid conflicts, strip the - # path down to the bare mimimum (which, on macOS, won't include any + # path down to the bare minimum (which, on macOS, won't include any # development dependencies). export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" export CMAKE_PREFIX_PATH=$BUILD_PREFIX From 9f09d48f3710409725beef1ca1be66da33855922 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 18:20:52 +1100 Subject: [PATCH 036/175] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9d45e2214ca..7e8e94ed9cf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Allow linking to zlib import library on Windows #8519 + [cubanpit, nulano] + - Detach PyQt6 QPixmap instance before returning #8509 [radarhere] From 7674b1a07a1dfdf1c0944aea1fefb95c4f62f5b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 18:43:12 +1100 Subject: [PATCH 037/175] Removed quotes after dropping support for Python 3.8 --- src/PIL/_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 0a7d87cc24b..335804b792f 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -47,7 +47,7 @@ class SupportsRead(Protocol[_T_co]): def read(self, __length: int = ...) -> _T_co: ... -StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] +StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] __all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] From 2303018ebf751b7d6bc9d0e3d830cdd5f529a5b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 9 Nov 2024 20:06:33 +1100 Subject: [PATCH 038/175] Removed use of os.path.realpath --- src/PIL/Image.py | 6 +++--- src/PIL/ImageFile.py | 2 +- src/PIL/ImageFont.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 44270392c4d..9b76ce8bd86 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2550,7 +2550,7 @@ def save( filename: str | bytes = "" open_fp = False if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) + filename = os.fspath(fp) open_fp = True elif fp == sys.stdout: try: @@ -2559,7 +2559,7 @@ def save( pass if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes - filename = os.path.realpath(os.fspath(fp.name)) + filename = os.fspath(fp.name) # may mutate self! self._ensure_mutable() @@ -3463,7 +3463,7 @@ def open( exclusive_fp = False filename: str | bytes = "" if is_path(fp): - filename = os.path.realpath(os.fspath(fp)) + filename = os.fspath(fp) if filename: fp = builtins.open(filename, "rb") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index d69d8456850..7f27d54dcc3 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -130,7 +130,7 @@ def __init__( if is_path(fp): # filename self.fp = open(fp, "rb") - self.filename = os.path.realpath(os.fspath(fp)) + self.filename = os.fspath(fp) self._exclusive_fp = True else: # stream diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b694b817e65..d8c2655609e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -270,7 +270,7 @@ def load_from_bytes(f: IO[bytes]) -> None: ) if is_path(font): - font = os.path.realpath(os.fspath(font)) + font = os.fspath(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: From 5bff2f3b2894ec6923c590d0c37b18177d0634bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 10 Nov 2024 21:32:54 +1100 Subject: [PATCH 039/175] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7e8e94ed9cf..f6ed5eb82fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.1.0 (unreleased) ------------------- +- Removed use of os.path.realpath #8545 + [radarhere] + - Allow linking to zlib import library on Windows #8519 [cubanpit, nulano] From 378df7a5b22e47a0652619bd2f3218674c579d91 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Nov 2024 22:41:57 +1100 Subject: [PATCH 040/175] Disable platform guessing instead of adding dependencies-prefix --- pyproject.toml | 4 +-- setup.py | 89 +++++++++++++++++++++----------------------------- 2 files changed, 40 insertions(+), 53 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 26fa4581706..abb7be6458c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,8 +95,8 @@ before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" -# Add an explicit dependencies prefix for macOS. -macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable dependencies-prefix=./build/deps/darwin" +# Disable platform guessing on macOS +macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" diff --git a/setup.py b/setup.py index e4ce04883dd..4770c5eb78f 100644 --- a/setup.py +++ b/setup.py @@ -344,11 +344,6 @@ def __iter__(self) -> Iterator[str]: for x in ("raqm", "fribidi") ] + [ - ( - "dependencies-prefix", - None, - "The prefix where build dependencies are located.", - ), ("disable-platform-guessing", None, "Disable platform guessing on Linux"), ("debug", None, "Debug logging"), ] @@ -360,7 +355,6 @@ def check_configuration(option: str, value: str) -> bool | None: return True if value in configuration.get(option, []) else None def initialize_options(self) -> None: - self.dependencies_prefix = configuration.get("dependencies-prefix", []) self.disable_platform_guessing = self.check_configuration( "platform-guessing", "disable" ) @@ -570,54 +564,47 @@ def build_extensions(self) -> None: ) elif sys.platform == "darwin": - if self.dependencies_prefix: - # Use the explicitly provided prefixes for dependencies. - for prefix in self.dependencies_prefix: - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) - else: - # Guess the dependency locations based on homebrew/fink/macports - # attempt to make sure we pick freetype2 over other versions - _add_directory(include_dirs, "/sw/include/freetype2") - _add_directory(include_dirs, "/sw/lib/freetype2/include") - # fink installation directories - _add_directory(library_dirs, "/sw/lib") - _add_directory(include_dirs, "/sw/include") - # darwin ports installation directories - _add_directory(library_dirs, "/opt/local/lib") - _add_directory(include_dirs, "/opt/local/include") - - # if Homebrew is installed, use its lib and include directories - try: - prefix = ( - subprocess.check_output(["brew", "--prefix"]) - .strip() - .decode("latin1") - ) - except Exception: - # Homebrew not installed - prefix = None + # attempt to make sure we pick freetype2 over other versions + _add_directory(include_dirs, "/sw/include/freetype2") + _add_directory(include_dirs, "/sw/lib/freetype2/include") + # fink installation directories + _add_directory(library_dirs, "/sw/lib") + _add_directory(include_dirs, "/sw/include") + # darwin ports installation directories + _add_directory(library_dirs, "/opt/local/lib") + _add_directory(include_dirs, "/opt/local/include") - ft_prefix = None + # if Homebrew is installed, use its lib and include directories + try: + prefix = ( + subprocess.check_output(["brew", "--prefix"]) + .strip() + .decode("latin1") + ) + except Exception: + # Homebrew not installed + prefix = None - if prefix: - # add Homebrew's include and lib directories - _add_directory(library_dirs, os.path.join(prefix, "lib")) - _add_directory(include_dirs, os.path.join(prefix, "include")) - _add_directory( - include_dirs, os.path.join(prefix, "opt", "zlib", "include") - ) - ft_prefix = os.path.join(prefix, "opt", "freetype") + ft_prefix = None - if ft_prefix and os.path.isdir(ft_prefix): - # freetype might not be linked into Homebrew's prefix - _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) - _add_directory(include_dirs, os.path.join(ft_prefix, "include")) - else: - # fall back to freetype from XQuartz if - # Homebrew's freetype is missing - _add_directory(library_dirs, "/usr/X11/lib") - _add_directory(include_dirs, "/usr/X11/include") + if prefix: + # add Homebrew's include and lib directories + _add_directory(library_dirs, os.path.join(prefix, "lib")) + _add_directory(include_dirs, os.path.join(prefix, "include")) + _add_directory( + include_dirs, os.path.join(prefix, "opt", "zlib", "include") + ) + ft_prefix = os.path.join(prefix, "opt", "freetype") + + if ft_prefix and os.path.isdir(ft_prefix): + # freetype might not be linked into Homebrew's prefix + _add_directory(library_dirs, os.path.join(ft_prefix, "lib")) + _add_directory(include_dirs, os.path.join(ft_prefix, "include")) + else: + # fall back to freetype from XQuartz if + # Homebrew's freetype is missing + _add_directory(library_dirs, "/usr/X11/lib") + _add_directory(include_dirs, "/usr/X11/include") # Add the macOS SDK path. sdk_path = self.get_macos_sdk_path() From 6fa775e324a27dac54ff8a9249553e06e1928051 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 12 Nov 2024 22:46:24 +1100 Subject: [PATCH 041/175] Platform guessing affects more than just Linux --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e78f87fe9e0..fea21ee3f44 100644 --- a/setup.py +++ b/setup.py @@ -344,7 +344,7 @@ def __iter__(self) -> Iterator[str]: for x in ("raqm", "fribidi") ] + [ - ("disable-platform-guessing", None, "Disable platform guessing on Linux"), + ("disable-platform-guessing", None, "Disable platform guessing"), ("debug", None, "Debug logging"), ] + [("add-imaging-libs=", None, "Add libs to _imaging build")] From 9dc690427f009f898ef74b8799569346eb0b3fd5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 13 Nov 2024 11:03:26 +0800 Subject: [PATCH 042/175] Correct the lookup of libfribidi on x86 macOS installs. --- .github/workflows/wheels-test.sh | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index d1db8e8a5ec..ce83a4278cd 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -7,21 +7,18 @@ if [[ "$OSTYPE" == "darwin"* ]]; then # However, we *do* need Homebrew to provide a copy of fribidi for # testing purposes so that we can verify the fribidi shim works as expected. if [[ "$(uname -m)" == "x86_64" ]]; then - # Use the "installed" location, rather than /usr/local, for two reasons: - # firstly, Homebrew allows libraries to be *installed*, but not linked; - # and secondly, we don't want any *other* leakage from /usr/local. - HOMEBREW_HOME=/usr/local/Homebrew + HOMEBREW_PREFIX=/usr/local else - HOMEBREW_HOME=/opt/homebrew + HOMEBREW_PREFIX=/opt/homebrew fi - $HOMEBREW_HOME/bin/brew install fribidi + $HOMEBREW_PREFIX/bin/brew install fribidi # Add the lib folder for fribidi so that the vendored library can be found. - # Don't use /opt/homebrew/lib directly - use the lib folder where the + # Don't use $HOMEWBREW_PREFIX/lib directly - use the lib folder where the # installed copy of fribidi is cellared. This ensures we don't pick up the # Homebrew version of any other library that we're dependent on (most notably, # freetype). - export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_HOME/lib/libfribidi.dylib)) + export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_PREFIX/lib/libfribidi.dylib)) elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then apk add curl fribidi else From 48c7eb22c0aeba649892dcb4766600f6dcfb6bdc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:45:52 +1100 Subject: [PATCH 043/175] Added default value for _Tile args --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/FliImagePlugin.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PcdImagePlugin.py | 2 +- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/XbmImagePlugin.py | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index fb1e301c084..35cc623e7d3 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -454,7 +454,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) - if hasattr(fp, "flush"): fp.flush() - ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0)]) fp.write(b"\n%%%%EndBinary\n") fp.write(b"grestore end\n") diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 666390be9ee..b534b30ab83 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -159,7 +159,7 @@ def _seek(self, frame: int) -> None: framesize = i32(s) self.decodermaxblock = framesize - self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)] + self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)] self.__offset += framesize diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 7f27d54dcc3..6c07c85e75a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -99,7 +99,7 @@ class _Tile(NamedTuple): codec_name: str extents: tuple[int, int, int, int] | None offset: int - args: tuple[Any, ...] | str | None + args: tuple[Any, ...] | str | None = None # diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index f3460a78730..50fb6a2d9d7 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -72,7 +72,7 @@ def _open(self) -> None: if s[:4] == b"DanM": self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))] else: - self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)] + self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] class MspDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index e8ea800a42c..ac40383f972 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -47,7 +47,7 @@ def _open(self) -> None: self._mode = "RGB" self._size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)] + self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)] def load_end(self) -> None: if self.tile_post_rotate: diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 010d3f941e1..01cc868b216 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -32,7 +32,7 @@ def _open(self) -> None: self._mode = "RGB" if channels == 3 else "RGBA" self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)] + self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())] class QoiDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index f3d490a840f..cf1a8b9f564 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -67,7 +67,7 @@ def _open(self) -> None: self._mode = "1" self._size = xsize, ysize - self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end(), None)] + self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end())] def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0)]) fp.write(b"};\n") From 871963b8ddab57fec9050135a5b58bceedce23db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:53:18 +1100 Subject: [PATCH 044/175] Replaced tuple args with mode string where equivalent --- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/DdsImagePlugin.py | 4 +--- src/PIL/FpxImagePlugin.py | 2 +- src/PIL/FtexImagePlugin.py | 2 +- src/PIL/GdImageFile.py | 2 +- src/PIL/ImtImagePlugin.py | 2 +- src/PIL/MspImagePlugin.py | 4 ++-- src/PIL/PixarImagePlugin.py | 4 +--- src/PIL/SpiderImagePlugin.py | 8 ++------ src/PIL/XVThumbImagePlugin.py | 4 +--- src/PIL/XpmImagePlugin.py | 4 +--- 11 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index e5605635e55..2d03af9d7dd 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -273,7 +273,7 @@ def _open(self) -> None: raise BLPFormatError(msg) self._mode = "RGBA" if self._blp_alpha_depth else "RGB" - self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)] class _BLPBaseDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 1b64082370c..9349e284178 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -560,9 +560,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + struct.pack("<4I", *rgba_mask) # dwRGBABitMask + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) ) - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] - ) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) def _accept(prefix: bytes) -> bool: diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8fef51076b4..4cfcb067d03 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -170,7 +170,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None: "raw", (x, y, x1, y1), i32(s, i) + 28, - (self.rawmode,), + self.rawmode, ) ) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index ddb469bc332..0516b760c61 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -95,7 +95,7 @@ def _open(self) -> None: self._mode = "RGBA" self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] elif format == Format.UNCOMPRESSED: - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")] else: msg = f"Invalid texture compression format: {repr(format)}" raise ValueError(msg) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index f1b4969f2c4..fc4801e9d1c 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -76,7 +76,7 @@ def _open(self) -> None: "raw", (0, 0) + self.size, 7 + true_color_offset + 4 + 256 * 4, - ("L", 0, 1), + "L", ) ] diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 594c56513cd..068cd5c33db 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -62,7 +62,7 @@ def _open(self) -> None: "raw", (0, 0) + self.size, self.fp.tell() - len(buffer), - (self.mode, 0, 1), + self.mode, ) ] diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 50fb6a2d9d7..ef6ae87f8c0 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -70,7 +70,7 @@ def _open(self) -> None: self._size = i16(s, 4), i16(s, 6) if s[:4] == b"DanM": - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] else: self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(o16(h)) # image body - ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")]) # diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 36f565f1c20..5c465bbdc5c 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -61,9 +61,7 @@ def _open(self) -> None: # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)] # diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 075073f9fe3..d7f457ae781 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -154,9 +154,7 @@ def _open(self) -> None: self.rawmode = "F;32F" self._mode = "F" - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)] self._fp = self.fp # FIXME: hack @property @@ -280,9 +278,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.writelines(hdr) rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] - ) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 5d1f201a454..75333354db2 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -74,9 +74,7 @@ def _open(self) -> None: self.palette = ImagePalette.raw("RGB", PALETTE) self.tile = [ - ImageFile._Tile( - "raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1) - ) + ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode) ] diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 1fc6c0c39d5..b985aa5dcd6 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -101,9 +101,7 @@ def _open(self) -> None: self._mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), "P")] def load_read(self, read_bytes: int) -> bytes: # From 84f5c7e5ba51afbeda6c4aaf42a31cea9ee8b082 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Nov 2024 22:52:36 +1100 Subject: [PATCH 045/175] Added default value for _Tile offset --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/XbmImagePlugin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 35cc623e7d3..36ba15ec50f 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -454,7 +454,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) - if hasattr(fp, "flush"): fp.flush() - ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0)]) + ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) fp.write(b"\n%%%%EndBinary\n") fp.write(b"grestore end\n") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 6c07c85e75a..bd0e92eb60d 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -98,7 +98,7 @@ def _tilesort(t: _Tile) -> int: class _Tile(NamedTuple): codec_name: str extents: tuple[int, int, int, int] | None - offset: int + offset: int = 0 args: tuple[Any, ...] | str | None = None diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index cf1a8b9f564..943a0447016 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size)]) fp.write(b"};\n") From 185a03f1a25ba56ae11d655c04645e0a50710dae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Nov 2024 12:05:06 +1100 Subject: [PATCH 046/175] Do not create new image when exif_transpose() is used in place --- src/PIL/ImageOps.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 44aad0c3ca1..bb29cc0d3e8 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -698,10 +698,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image 8: Image.Transpose.ROTATE_90, }.get(orientation) if method is not None: - transposed_image = image.transpose(method) if in_place: - image.im = transposed_image.im - image._size = transposed_image._size + image.im = image.im.transpose(method) + image._size = image.im.size + else: + transposed_image = image.transpose(method) exif_image = image if in_place else transposed_image exif = exif_image.getexif() From 54f23345d2fd1894c7cf89475e385fcbb3d3f16d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 16 Nov 2024 09:58:29 +0800 Subject: [PATCH 047/175] More tweaks from code review. --- .github/workflows/wheels-dependencies.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f4106429d46..ce2e40103d0 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -19,14 +19,13 @@ if [[ "$(uname -s)" == "Darwin" ]]; then # Install them into `build/deps/darwin` WORKDIR=$(pwd)/build/darwin BUILD_PREFIX=$(pwd)/build/deps/darwin - PLAT=$CIBW_ARCHS else # Build prefix will default to /usr/local WORKDIR=$(pwd)/build - PLAT=$CIBW_ARCHS MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_VER=${AUDITWHEEL_POLICY:9} fi +PLAT=$CIBW_ARCHS # Define custom utilities source wheels/multibuild/common_utils.sh @@ -119,9 +118,9 @@ function build { build_libjpeg_turbo if [ -n "$IS_MACOS" ]; then # Custom tiff build to include jpeg; by default, configure won't include - # headers/libs in the custom macOS prefix. Explicitly disable webp and - # zstd, because on x86_64 macs, it will pick up the Homebrew versions of - # webp and zstd from /usr/local. + # headers/libs in the custom macOS prefix. Explicitly disable webp, + # libdeflate and zstd, because on x86_64 macs, it will pick up the + # Homebrew versions of those libraries from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ --disable-webp --disable-zstd --disable-libdeflate From 6136a92d061e77e622c974c35aa0605f96eb05e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Nov 2024 09:48:28 +0000 Subject: [PATCH 048/175] Update codecov/codecov-action action to v5 --- .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 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 656054e8924..b906ada92e0 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -135,7 +135,7 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: GHA_Cygwin diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 03608319a60..cc5f9d4a5a9 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -100,7 +100,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bfd393db5a2..a14e7e1ed7f 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -85,7 +85,7 @@ jobs: .ci/test.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 9b5da137d8f..d1924eb2018 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -215,7 +215,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c33309bbf03..1405c64f9aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,7 +156,7 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From c70dacff62c07341c4bb9d9c28e26e4e703ab5ae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 16 Nov 2024 21:44:00 +1100 Subject: [PATCH 049/175] Updated from deprecated "file" to "files" --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index b906ada92e0..2076cad7a9a 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -137,7 +137,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Cygwin name: Cygwin Python 3.${{ matrix.python-minor-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a14e7e1ed7f..cca25edba42 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -87,7 +87,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Windows name: "MSYS2 MinGW" token: ${{ secrets.CODECOV_ORG_TOKEN }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index d1924eb2018..d905a392585 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -217,7 +217,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 with: - file: ./coverage.xml + files: ./coverage.xml flags: GHA_Windows name: ${{ runner.os }} Python ${{ matrix.python-version }} token: ${{ secrets.CODECOV_ORG_TOKEN }} From 96b898c826982c5b99f2a9387e0d2011a73fe20a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 18 Nov 2024 09:56:45 +0800 Subject: [PATCH 050/175] A couple more cleanups from code review. --- .github/workflows/wheels-dependencies.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index ce2e40103d0..f86099af16e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -62,8 +62,7 @@ BROTLI_VERSION=1.1.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi - # This essentially duplicates the Homebrew recipe: - # /~https://github.com/Homebrew/homebrew-core/blob/master/Formula/p/pkg-config.rb + # This essentially duplicates the Homebrew recipe ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ @@ -108,7 +107,6 @@ function build { if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib - build_simple libXdmcp 1.1.5 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc @@ -123,7 +121,7 @@ function build { # Homebrew versions of those libraries from /usr/local. build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ - --disable-webp --disable-zstd --disable-libdeflate + --disable-webp --disable-libdeflate --disable-zstd else build_tiff fi @@ -134,6 +132,9 @@ function build { ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" + if [[ -n "$IS_MACOS" ]]; then + CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + fi build_libwebp CFLAGS=$ORIGINAL_CFLAGS @@ -141,7 +142,7 @@ function build { if [ -n "$IS_MACOS" ]; then # Custom freetype build - build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --without-harfbuzz + build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no else build_freetype fi @@ -175,7 +176,7 @@ if [[ -n "$IS_MACOS" ]]; then export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" export CMAKE_PREFIX_PATH=$BUILD_PREFIX - # Link the brew command into our isolated build directory. + # Ensure the basic structure of the build prefix directory exists. mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" From e02e4aaf1de9503e6e88a60f292998d174d23034 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 5 Nov 2024 19:31:54 +1100 Subject: [PATCH 051/175] Updated harfbuzz to 10.1.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f86099af16e..89226810a09 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -38,7 +38,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=10.0.1 +HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.4 OPENJPEG_VERSION=2.5.2 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c8332d11c7e..5606ac7f7ed 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -112,7 +112,7 @@ def cmd_msbuild( "BROTLI": "1.1.0", "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", - "HARFBUZZ": "10.0.1", + "HARFBUZZ": "10.1.0", "JPEGTURBO": "3.0.4", "LCMS2": "2.16", "LIBPNG": "1.6.44", From c7a9582eab34bc46b74c245aa137424b9cfaee74 Mon Sep 17 00:00:00 2001 From: Tom Flanagan Date: Tue, 19 Nov 2024 20:40:14 -0800 Subject: [PATCH 052/175] Fix file position desync when calling into libtiff --- Tests/test_libtiff_file_position.py | 23 +++++++++++++++++++++++ src/PIL/TiffImagePlugin.py | 17 +++++++++-------- 2 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 Tests/test_libtiff_file_position.py diff --git a/Tests/test_libtiff_file_position.py b/Tests/test_libtiff_file_position.py new file mode 100644 index 00000000000..d614ea8eca1 --- /dev/null +++ b/Tests/test_libtiff_file_position.py @@ -0,0 +1,23 @@ +from __future__ import annotations + + +import pytest + +from PIL import Image + + +@pytest.mark.parametrize('test_file', [ + 'Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif', + 'Tests/images/old-style-jpeg-compression.tif', +]) +def test_libtiff_exif_loading(test_file) -> None: + # loading image before exif + im1 = Image.open(open(test_file, 'rb', buffering=1048576)) + im1.load() + exif1 = dict(im1.getexif()) + + # loading exif before image + im2 = Image.open(open(test_file, 'rb', buffering=1048576)) + exif2 = dict(im2.getexif()) + + assert exif1 == exif2 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a5f..72628677278 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1216,10 +1216,6 @@ def seek(self, frame: int) -> None: def _seek(self, frame: int) -> None: self.fp = self._fp - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() - while len(self._frame_pos) <= frame: if not self.__next: msg = "no more images in TIFF file" @@ -1303,10 +1299,6 @@ def load_end(self) -> None: if not self.is_animated: self._close_exclusive_fp_after_loading = True - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() - # load IFD data from fp before it is closed exif = self.getexif() for key in TiffTags.TAGS_V2_GROUPS: @@ -1381,8 +1373,17 @@ def _load_libtiff(self) -> Image.core.PixelAccess | None: logger.debug("have fileno, calling fileno version of the decoder.") if not close_self_fp: self.fp.seek(0) + # Save and restore the file position, because libtiff will move it + # outside of the python runtime, and that will confuse + # io.BufferedReader and possible others. + # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), + # because the buffer read head already may not equal the actual + # file position, and fp.seek() may just adjust it's internal + # pointer and not actually seek the OS file handle. + pos = os.lseek(fp, 0, os.SEEK_CUR) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") + os.lseek(fp, pos, os.SEEK_SET) else: # we have something else. logger.debug("don't have fileno or getvalue. just reading") From 44cc0bebb8998897f03d77479df750f208d9d00e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 05:52:50 +0000 Subject: [PATCH 053/175] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_libtiff_file_position.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Tests/test_libtiff_file_position.py b/Tests/test_libtiff_file_position.py index d614ea8eca1..43ed82c30e1 100644 --- a/Tests/test_libtiff_file_position.py +++ b/Tests/test_libtiff_file_position.py @@ -1,23 +1,25 @@ from __future__ import annotations - import pytest from PIL import Image -@pytest.mark.parametrize('test_file', [ - 'Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif', - 'Tests/images/old-style-jpeg-compression.tif', -]) +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", + "Tests/images/old-style-jpeg-compression.tif", + ], +) def test_libtiff_exif_loading(test_file) -> None: # loading image before exif - im1 = Image.open(open(test_file, 'rb', buffering=1048576)) + im1 = Image.open(open(test_file, "rb", buffering=1048576)) im1.load() exif1 = dict(im1.getexif()) # loading exif before image - im2 = Image.open(open(test_file, 'rb', buffering=1048576)) + im2 = Image.open(open(test_file, "rb", buffering=1048576)) exif2 = dict(im2.getexif()) assert exif1 == exif2 From cb1653f6272ebc543530ae03cd7cf8577ed13231 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Nov 2024 22:33:23 +1100 Subject: [PATCH 054/175] Updated comment --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 72628677278..08bb7891512 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1374,7 +1374,7 @@ def _load_libtiff(self) -> Image.core.PixelAccess | None: if not close_self_fp: self.fp.seek(0) # Save and restore the file position, because libtiff will move it - # outside of the python runtime, and that will confuse + # outside of the Python runtime, and that will confuse # io.BufferedReader and possible others. # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), # because the buffer read head already may not equal the actual From 925db4552658ac0d8c037b0ac0b39bd7eb96f8f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Nov 2024 22:40:29 +1100 Subject: [PATCH 055/175] Moved test --- Tests/test_file_libtiff.py | 19 +++++++++++++++++++ Tests/test_libtiff_file_position.py | 25 ------------------------- 2 files changed, 19 insertions(+), 25 deletions(-) delete mode 100644 Tests/test_libtiff_file_position.py diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 62f8719af53..9c49b1534ed 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1098,6 +1098,25 @@ def test_exif_transpose(self) -> None: assert_image_similar(base_im, im, 0.7) + @pytest.mark.parametrize( + "test_file", + [ + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", + "Tests/images/old-style-jpeg-compression.tif", + ], + ) + def test_buffering(self, test_file: str) -> None: + # load exif first + with Image.open(open(test_file, "rb", buffering=1048576)) as im: + exif = dict(im.getexif()) + + # load image before exif + with Image.open(open(test_file, "rb", buffering=1048576)) as im2: + im2.load() + exif_after_load = dict(im2.getexif()) + + assert exif == exif_after_load + @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") def test_sampleformat_not_corrupted(self) -> None: # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted diff --git a/Tests/test_libtiff_file_position.py b/Tests/test_libtiff_file_position.py deleted file mode 100644 index 43ed82c30e1..00000000000 --- a/Tests/test_libtiff_file_position.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -import pytest - -from PIL import Image - - -@pytest.mark.parametrize( - "test_file", - [ - "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", - "Tests/images/old-style-jpeg-compression.tif", - ], -) -def test_libtiff_exif_loading(test_file) -> None: - # loading image before exif - im1 = Image.open(open(test_file, "rb", buffering=1048576)) - im1.load() - exif1 = dict(im1.getexif()) - - # loading exif before image - im2 = Image.open(open(test_file, "rb", buffering=1048576)) - exif2 = dict(im2.getexif()) - - assert exif1 == exif2 From 82dfbc35b190a818a1cbdbee6524eaf31605b7bd Mon Sep 17 00:00:00 2001 From: Tom Flanagan Date: Wed, 20 Nov 2024 05:21:14 -0800 Subject: [PATCH 056/175] Update src/PIL/TiffImagePlugin.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 72628677278..08bb7891512 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1374,7 +1374,7 @@ def _load_libtiff(self) -> Image.core.PixelAccess | None: if not close_self_fp: self.fp.seek(0) # Save and restore the file position, because libtiff will move it - # outside of the python runtime, and that will confuse + # outside of the Python runtime, and that will confuse # io.BufferedReader and possible others. # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(), # because the buffer read head already may not equal the actual From bfe40b5129e9250243b1d425172c18f7c4e15e10 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Nov 2024 19:22:39 +1100 Subject: [PATCH 057/175] Prevent Cygwin bash from being used by codecov-action --- .github/workflows/test-cygwin.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 2076cad7a9a..5b0a0394688 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -133,6 +133,7 @@ jobs: - name: After success run: | bash.exe .ci/after_success.sh + rm C:\cygwin\bin\bash.EXE - name: Upload coverage uses: codecov/codecov-action@v5 From 69c9a7ffcfd42b0486f02f17119c6d05f1d04969 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 20 Nov 2024 20:03:15 +1100 Subject: [PATCH 058/175] Use zlib-ng on Linux --- .github/workflows/wheels-dependencies.sh | 20 +++++++++++++++++--- Tests/check_wheel.py | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f86099af16e..5d23fc036ee 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -50,10 +50,10 @@ if [[ -n "$IS_MACOS" ]]; then else GIFLIB_VERSION=5.2.1 fi -if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then +if [[ -n "$IS_MACOS" ]]; then ZLIB_VERSION=1.3.1 else - ZLIB_VERSION=1.2.8 + ZLIB_NG_VERSION=2.2.2 fi LIBWEBP_VERSION=1.4.0 BZIP2_VERSION=1.0.8 @@ -74,6 +74,16 @@ function build_pkg_config { touch pkg-config-stamp } +function build_zlib_ng { + if [ -e zlib-stamp ]; then return; fi + fetch_unpack /~https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz + (cd zlib-ng-$ZLIB_NG_VERSION \ + && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ + && make -j4 \ + && make install) + touch zlib-stamp +} + function build_brotli { if [ -e brotli-stamp ]; then return; fi local cmake=$(get_modern_cmake) @@ -101,7 +111,11 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - build_new_zlib + if [ -n "$IS_MACOS" ]; then + build_new_zlib + else + build_zlib_ng + fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 374e48e8ad3..787d4f7431a 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -40,7 +40,7 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") - else: + elif sys.platform == "darwin": expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features From 9c3d8fb581cab18abb60117f08358b51b18a44b0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2024 11:02:50 +1100 Subject: [PATCH 059/175] Do not install Pyroma, to use only system packages --- .github/workflows/test-mingw.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index bfd393db5a2..fd8faba71bf 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -68,12 +68,13 @@ jobs: mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-olefile \ + mingw-w64-x86_64-python3-pip \ mingw-w64-x86_64-python3-setuptools \ + mingw-w64-x86_64-python-pytest \ + mingw-w64-x86_64-python-pytest-cov \ + mingw-w64-x86_64-python-pytest-timeout \ mingw-w64-x86_64-python-pyqt6 - python3 -m ensurepip - python3 -m pip install pyroma pytest pytest-cov pytest-timeout - pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow From 30f03b2bff689af1f395c183cf991bc34f8b0bab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2024 11:05:33 +1100 Subject: [PATCH 060/175] Removed SETUPTOOLS_USE_DISTUTILS="stdlib" --- .github/workflows/test-mingw.yml | 3 +-- docs/installation/building-from-source.rst | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index fd8faba71bf..93fe8a06465 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -69,7 +69,6 @@ jobs: mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools \ mingw-w64-x86_64-python-pytest \ mingw-w64-x86_64-python-pytest-cov \ mingw-w64-x86_64-python-pytest-timeout \ @@ -78,7 +77,7 @@ jobs: pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . + run: CFLAGS="-coverage" python3 -m pip install . - name: Test Pillow run: | diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 4b517582786..19294f72c1c 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -195,11 +195,6 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libraqm - https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with - MSYS2. To workaround this, before installing Pillow you must run:: - - export SETUPTOOLS_USE_DISTUTILS=stdlib - .. tab:: FreeBSD .. Note:: Only FreeBSD 10 and 11 tested From 16e409a8d28fecbf83cd67409224e65ebb371a93 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 24 Nov 2024 13:46:28 +0200 Subject: [PATCH 061/175] Test 3.13t (free-threaded) from Quansight-Labs/setup-python on Linux/macOS --- .github/workflows/test.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1405c64f9aa..83a696f5f8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: ] python-version: [ "pypy3.10", + "3.13t", "3.13", "3.12", "3.11", @@ -52,14 +53,14 @@ jobs: - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.10", PYTHONOPTIMIZE: 2 } # Free-threaded - - { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true } + - { python-version: "3.13t", disable-gil: true } # M1 only available for 3.10+ - { os: "macos-13", python-version: "3.9" } exclude: - { os: "macos-latest", python-version: "3.9" } runs-on: ${{ matrix.os }} - name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v4 @@ -67,8 +68,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - if: "${{ !matrix.disable-gil }}" + uses: Quansight-Labs/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -77,13 +77,6 @@ jobs: ".ci/*.sh" "pyproject.toml" - - name: Set up Python ${{ matrix.python-version }} (free-threaded) - uses: deadsnakes/action@v3.2.0 - if: "${{ matrix.disable-gil }}" - with: - python-version: ${{ matrix.python-version }} - nogil: ${{ matrix.disable-gil }} - - name: Set PYTHON_GIL if: "${{ matrix.disable-gil }}" run: | From 540a0b73a7ebb3ec5571e3ea027c73cbe95b2fc6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 23 Nov 2024 00:20:40 +1100 Subject: [PATCH 062/175] Use installed cmake --- .github/workflows/wheels-dependencies.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index f86099af16e..ddda43423cd 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -76,10 +76,9 @@ function build_pkg_config { function build_brotli { if [ -e brotli-stamp ]; then return; fi - local cmake=$(get_modern_cmake) local out_dir=$(fetch_unpack /~https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) (cd $out_dir \ - && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) touch brotli-stamp } From 7dcf4d8ab319348239d4f8695c6d3366bc257cbf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Nov 2024 16:43:21 +1100 Subject: [PATCH 063/175] Added logging to fixIFD() --- src/PIL/TiffImagePlugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6bf39b75a5f..a246994efee 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2202,6 +2202,12 @@ def fixIFD(self) -> None: if tag in self.Tags: cur_pos = self.f.tell() + tagname = TiffTags.lookup(tag).name + typname = TYPES.get(field_type, "unknown") + msg = f"fixIFD: {tagname} ({tag}) - type: {typname} ({field_type})" + msg += f"- type size: {field_size} - count: {count}" + logger.debug(msg) + if is_local: self._fixOffsets(count, field_size) self.f.seek(cur_pos + 4) From 5174c596ca6c78725efdb2cda8ace39f11d8e215 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:06:47 +0200 Subject: [PATCH 064/175] Release drafter: move removals, deprecations, documentation up, and uncategorised changes last --- .github/release-drafter.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 3711d91f0d5..de0ab480519 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -3,18 +3,19 @@ tag-template: "$NEXT_MINOR_VERSION" change-template: '- $TITLE #$NUMBER [@$AUTHOR]' categories: - - title: "Dependencies" - label: "Dependency" + - title: "Removals" + label: "Removal" - title: "Deprecations" label: "Deprecation" - title: "Documentation" label: "Documentation" - - title: "Removals" - label: "Removal" + - title: "Dependencies" + label: "Dependency" - title: "Testing" label: "Testing" - title: "Type hints" label: "Type hints" + - title: "Other changes" exclude-labels: - "changelog: skip" @@ -23,6 +24,4 @@ template: | https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html - ## Changes - $CHANGES From 02250e81b729ddb32952f96e57bf9d952ed70cf8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 28 Nov 2024 07:58:10 +1100 Subject: [PATCH 065/175] Added link to GitHub releases --- CHANGES.rst | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f6ed5eb82fb..dfbbd24b331 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,26 +2,12 @@ Changelog (Pillow) ================== -11.1.0 (unreleased) -------------------- - -- Removed use of os.path.realpath #8545 - [radarhere] - -- Allow linking to zlib import library on Windows #8519 - [cubanpit, nulano] - -- Detach PyQt6 QPixmap instance before returning #8509 - [radarhere] +11.1.0 and newer +---------------- -- Corrected EMF DPI #8485 - [radarhere] +See GitHub Releases: -- Fix IFDRational with a zero denominator #8474 - [radarhere] - -- Fixed disabling a feature during install #8469 - [radarhere] +- /~https://github.com/python-pillow/Pillow/releases 11.0.0 (2024-10-15) ------------------- From c36a5b669b225fdcb6b4b3a41fc5b5e647a651c1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:29:05 +0200 Subject: [PATCH 066/175] Update/remove references to CHANGES.rst --- .github/CONTRIBUTING.md | 1 - README.md | 2 +- RELEASING.md | 2 -- codecov.yml | 2 +- pyproject.toml | 2 +- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d03fcf0d9da..ba2b7d8ed26 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,7 +19,6 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Follow PEP 8. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. - Include [release notes](/~https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. -- Do not add to the [changelog](/~https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged. ## Reporting Issues diff --git a/README.md b/README.md index 5bbebaccb4a..057d0acf0ce 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ The core image library is designed for fast access to data stored in a few basic - [Issues](/~https://github.com/python-pillow/Pillow/issues) - [Pull requests](/~https://github.com/python-pillow/Pillow/pulls) - [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) -- [Changelog](/~https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) +- [Changelog](/~https://github.com/python-pillow/Pillow/releases) - [Pre-fork](/~https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) ## Report a Vulnerability diff --git a/RELEASING.md b/RELEASING.md index 9e6ec5dd4c1..ebdbb6406e8 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,7 +12,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] 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 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. * [ ] Create branch and tag for release e.g.: ```bash @@ -34,7 +33,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. Released as needed for security, installation or critical bug fixes. * [ ] Make necessary changes in `main` branch. -* [ ] Update `CHANGES.rst`. * [ ] Check out release branch e.g.: ```bash git checkout -t remotes/origin/5.2.x diff --git a/codecov.yml b/codecov.yml index 8646576bb44..84920238ffc 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,7 +1,7 @@ # Documentation: https://docs.codecov.com/docs/codecov-yaml codecov: - # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" + # Avoid "Missing base report" due to committing with "[CI skip]" # /~https://github.com/codecov/support/issues/363 # https://docs.codecov.com/docs/comparing-commits allow_coverage_offsets: true diff --git a/pyproject.toml b/pyproject.toml index c6d95f2abe3..1c7b2c4b28e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ optional-dependencies.typing = [ optional-dependencies.xmp = [ "defusedxml", ] -urls.Changelog = "/~https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" +urls.Changelog = "/~https://github.com/python-pillow/Pillow/releases" urls.Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" urls.Homepage = "https://python-pillow.org" From 4986609938c6fd38af6efd8501fe157b0443e5bc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Nov 2024 23:44:32 +1100 Subject: [PATCH 067/175] Use zlib-ng on macOS --- .github/workflows/wheels-dependencies.sh | 12 ++---------- Tests/check_wheel.py | 2 -- pyproject.toml | 1 + 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5d23fc036ee..a2aec07c32d 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -50,11 +50,7 @@ if [[ -n "$IS_MACOS" ]]; then else GIFLIB_VERSION=5.2.1 fi -if [[ -n "$IS_MACOS" ]]; then - ZLIB_VERSION=1.3.1 -else - ZLIB_NG_VERSION=2.2.2 -fi +ZLIB_NG_VERSION=2.2.2 LIBWEBP_VERSION=1.4.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 @@ -111,11 +107,7 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - if [ -n "$IS_MACOS" ]; then - build_new_zlib - else - build_zlib_ng - fi + build_zlib_ng build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 787d4f7431a..563be0b7489 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -40,7 +40,5 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") - elif sys.platform == "darwin": - expected_features.remove("zlib_ng") assert set(features.get_supported_features()) == expected_features diff --git a/pyproject.toml b/pyproject.toml index c6d95f2abe3..af09ccbc1f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib" [tool.black] exclude = "wheels/multibuild" From 10527918e3570f1e6772ac90db42d2288cce7ae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 29 Nov 2024 17:13:07 +1100 Subject: [PATCH 068/175] Use brew formula to install libraqm --- docs/installation/building-from-source.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 19294f72c1c..ff60120aedc 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -148,13 +148,7 @@ Many of Pillow's features require external libraries: The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - brew install libjpeg libtiff little-cms2 openjpeg webp - - To install libraqm on macOS use Homebrew to install its dependencies:: - - brew install freetype harfbuzz fribidi - - Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + brew install libjpeg libraqm libtiff little-cms2 openjpeg webp .. tab:: Windows From 1fab591f42fa5a9502fade7873481b567c033849 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Nov 2024 21:17:31 +1100 Subject: [PATCH 069/175] Python 3.12 is tested on MinGW --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index b9d633142e7..35f863374d1 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -55,7 +55,7 @@ These platforms are built and tested for every change. | +----------------------------+---------------------+ | | 3.13 | x86 | | +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86-64 | +| | 3.12 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ From 1d15ce8c494075f521600c21b9efb0d5eaa9aa03 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Nov 2024 23:17:42 +1100 Subject: [PATCH 070/175] Updated build option error messages --- setup.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index fbd23a5685a..eea21fa3407 100644 --- a/setup.py +++ b/setup.py @@ -393,13 +393,14 @@ def finalize_options(self) -> None: self.feature.required.discard(x) _dbg("Disabling %s", x) if getattr(self, f"enable_{x}"): - msg = f"Conflicting options: --enable-{x} and --disable-{x}" + msg = f"Conflicting options: '-C {x}=enable' and '-C {x}=disable'" raise ValueError(msg) if x == "freetype": - _dbg("--disable-freetype implies --disable-raqm") + _dbg("'-C freetype=disable' implies '-C raqm=disable'") if getattr(self, "enable_raqm"): msg = ( - "Conflicting options: --enable-raqm and --disable-freetype" + "Conflicting options: " + "'-C raqm=enable' and '-C freetype=disable'" ) raise ValueError(msg) setattr(self, "disable_raqm", True) @@ -407,15 +408,17 @@ def finalize_options(self) -> None: _dbg("Requiring %s", x) self.feature.required.add(x) if x == "raqm": - _dbg("--enable-raqm implies --enable-freetype") + _dbg("'-C raqm=enable' implies '-C freetype=enable'") self.feature.required.add("freetype") for x in ("raqm", "fribidi"): if getattr(self, f"vendor_{x}"): if getattr(self, "disable_raqm"): - msg = f"Conflicting options: --vendor-{x} and --disable-raqm" + msg = f"Conflicting options: '-C {x}=vendor' and '-C raqm=disable'" raise ValueError(msg) if x == "fribidi" and not getattr(self, "vendor_raqm"): - msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm" + msg = ( + f"Conflicting options: '-C {x}=vendor' and not '-C raqm=vendor'" + ) raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) @@ -1047,7 +1050,7 @@ def debug_build() -> bool: msg = f""" The headers or library files could not be found for {str(err)}, -which was requested by the option flag --enable-{str(err)} +which was requested by the option flag '-C {str(err)}=enable' """ sys.stderr.write(msg) From 513fe2ca2d23c7f1685b089fc6087ab02b2828df Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:15:54 +0200 Subject: [PATCH 071/175] Only use start year in copyright, remove end years --- LICENSE | 2 +- docs/COPYING | 2 +- docs/conf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 8837c290c30..10dd42d9eda 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-2024 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey A. Clark and contributors Like PIL, Pillow is licensed under the open source MIT-CMU License: diff --git a/docs/COPYING b/docs/COPYING index d5ee19f81a6..17fba5b87ff 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-2024 by Jeffrey A. Clark and contributors + Copyright © 2010 by Jeffrey A. Clark 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 b81d86c6ca2..e1e3f1b8f8a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,7 +55,7 @@ project = "Pillow (PIL Fork)" copyright = ( "1995-2011 Fredrik Lundh and contributors, " - "2010-2024 Jeffrey A. Clark and contributors." + "2010 Jeffrey A. Clark and contributors." ) author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" From 0000729f2a79da62ba64332484103ffacf1700a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:34:54 +0000 Subject: [PATCH 072/175] [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.7.2 → v0.8.1](/~https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.2...v0.8.1) - [github.com/PyCQA/bandit: 1.7.10 → 1.8.0](/~https://github.com/PyCQA/bandit/compare/1.7.10...1.8.0) - [github.com/pre-commit/mirrors-clang-format: v19.1.3 → v19.1.4](/~https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.3...v19.1.4) - [github.com/python-jsonschema/check-jsonschema: 0.29.4 → 0.30.0](/~https://github.com/python-jsonschema/check-jsonschema/compare/0.29.4...0.30.0) - [github.com/abravalheri/validate-pyproject: v0.22 → v0.23](/~https://github.com/abravalheri/validate-pyproject/compare/v0.22...v0.23) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ddc98fdc356..f91260c724f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: /~https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.2 + rev: v0.8.1 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: /~https://github.com/PyCQA/bandit - rev: 1.7.10 + rev: 1.8.0 hooks: - id: bandit args: [--severity-level=high] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: /~https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.3 + rev: v19.1.4 hooks: - id: clang-format types: [c] @@ -50,7 +50,7 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: /~https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.4 + rev: 0.30.0 hooks: - id: check-github-workflows - id: check-readthedocs @@ -67,7 +67,7 @@ repos: - id: pyproject-fmt - repo: /~https://github.com/abravalheri/validate-pyproject - rev: v0.22 + rev: v0.23 hooks: - id: validate-pyproject additional_dependencies: [trove-classifiers>=2024.10.12] From 0ab21dff1ed0b16944a5458ab73dfa3a5c060b88 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:29:38 +0200 Subject: [PATCH 073/175] Fix new Ruff errors --- src/PIL/ImImagePlugin.py | 2 +- src/PIL/Image.py | 21 ++++++--------------- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 13 +++++++------ src/PIL/_typing.py | 2 +- 7 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index f9f47348c66..b4215a0b1e5 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -357,7 +357,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: name = "".join([name[: 92 - len(ext)], ext]) fp.write(f"Name: {name}\r\n".encode("ascii")) - fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii")) + fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii")) fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) if im.mode in ["P", "PA"]: fp.write(b"Lut: 1\r\n") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9b76ce8bd86..1e289b6c38b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -692,13 +692,10 @@ def __eq__(self, other: object) -> bool: ) def __repr__(self) -> str: - return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( - self.__class__.__module__, - self.__class__.__name__, - self.mode, - self.size[0], - self.size[1], - id(self), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]} " + f"at 0x{id(self):X}>" ) def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: @@ -707,14 +704,8 @@ def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: # Same as __repr__ but without unpredictable id(self), # to keep Jupyter notebook `text/plain` output stable. p.text( - "<%s.%s image mode=%s size=%dx%d>" - % ( - self.__class__.__module__, - self.__class__.__name__, - self.mode, - self.size[0], - self.size[1], - ) + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>" ) def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e5e..ab6a2f497fb 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -72,7 +72,7 @@ def APP(self: JpegImageFile, marker: int) -> None: n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) - app = "APP%d" % (marker & 15) + app = f"APP{marker & 15}" self.app[app] = s # compatibility self.applist.append((app, s)) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 8445d5cc740..32436cea3af 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -86,7 +86,7 @@ def _open(self) -> None: elif bits == 1 and planes in (2, 4): mode = "P" - rawmode = "P;%dL" % planes + rawmode = f"P;{planes}L" self.palette = ImagePalette.raw("RGB", s[16:64]) elif version == 5 and bits == 8 and planes == 1: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 4e12272041d..4b97992a31f 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -523,7 +523,7 @@ def chunk_cHRM(self, pos: int, length: int) -> bytes: assert self.fp is not None s = ImageFile._safe_read(self.fp, length) - raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) + raw_vals = struct.unpack(f">{len(s) // 4}I", s) self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) return s diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 08bb7891512..9ee55347aaf 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -935,9 +935,9 @@ def load(self, fp: IO[bytes]) -> None: self._tagdata[tag] = data self.tagtype[tag] = typ - msg += " - value: " + ( - "" % size if size > 32 else repr(data) - ) + bytes_value = size if size > 32 else repr(data) + msg += f" - value: " + logger.debug(msg) (self.next,) = ( @@ -981,9 +981,10 @@ def tobytes(self, offset: int = 0) -> bytes: tagname = TiffTags.lookup(tag, self.group).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" - msg += " - value: " + ( - "" % len(data) if len(data) >= 16 else str(values) + bytes_value = len(data) if len(data) >= 16 else str(values) + msg = ( + f"save: {tagname} ({tag}) - type: {typname} ({typ})" + f" - value: " ) logger.debug(msg) diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 335804b792f..34a9a81e11f 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -44,7 +44,7 @@ def __class_getitem__(cls, item: Any) -> type[bool]: class SupportsRead(Protocol[_T_co]): - def read(self, __length: int = ...) -> _T_co: ... + def read(self, length: int = ..., /) -> _T_co: ... StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] From c16ae6fefa6cb939d274c95d1969889dced9dd15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 09:19:26 +1100 Subject: [PATCH 074/175] Do not describe raw data as a table --- src/PIL/TiffImagePlugin.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 9ee55347aaf..be4a57b7445 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -935,8 +935,8 @@ def load(self, fp: IO[bytes]) -> None: self._tagdata[tag] = data self.tagtype[tag] = typ - bytes_value = size if size > 32 else repr(data) - msg += f" - value: " + msg += " - value: " + msg += f"" if size > 32 else repr(data) logger.debug(msg) @@ -981,11 +981,8 @@ def tobytes(self, offset: int = 0) -> bytes: tagname = TiffTags.lookup(tag, self.group).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - bytes_value = len(data) if len(data) >= 16 else str(values) - msg = ( - f"save: {tagname} ({tag}) - type: {typname} ({typ})" - f" - value: " - ) + msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: " + msg += f"" if len(data) >= 16 else str(values) logger.debug(msg) # count is sum of lengths for string and arbitrary data From 80c562573a4b5723b1094fb9b07a582912e3d62b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 02:28:52 +0000 Subject: [PATCH 075/175] Update dependency cibuildwheel to v2.22.0 --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index c4511439ccb..833aca23d4a 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.21.3 +cibuildwheel==2.22.0 From f4a9304bb358f45a088d3c237ada88703e0e0d8e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 13:35:00 +1100 Subject: [PATCH 076/175] Use CIBW_ENABLE --- .github/workflows/wheels.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 45f18634100..c5e55aa621d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -85,7 +85,7 @@ jobs: CIBW_ARCHS: "aarch64" # Likewise, select only one Python version per job to speed this up. CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" - CIBW_PRERELEASE_PYTHONS: True + CIBW_ENABLE: cpython-prerelease # Extra options for manylinux. CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} @@ -150,10 +150,9 @@ jobs: env: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} - CIBW_FREE_THREADED_SUPPORT: True + CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} - CIBW_PRERELEASE_PYTHONS: True CIBW_SKIP: pp39-* MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} @@ -228,8 +227,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" - CIBW_FREE_THREADED_SUPPORT: True - CIBW_PRERELEASE_PYTHONS: True + CIBW_ENABLE: cpython-prerelease cpython-freethreading CIBW_SKIP: pp39-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm From 3ce33a253a1bd76fdde6eb557cf271b3b2f01553 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 3 Dec 2024 20:14:03 +1100 Subject: [PATCH 077/175] self.tile is not None --- 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 7f27d54dcc3..9836cd68884 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -120,7 +120,7 @@ def __init__( self.custom_mimetype: str | None = None self.tile: list[_Tile] = [] - """ A list of tile descriptors, or ``None`` """ + """ A list of tile descriptors """ self.readonly = 1 # until we know better From c5474ed43363e24602c92e5ca7b37899ce37b9a5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 6 Dec 2024 20:26:07 +0200 Subject: [PATCH 078/175] Replace python-pillow.org with python-pillow.github.io --- docs/handbook/tutorial.rst | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 3df8e0d20be..f771ae7aea5 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -678,7 +678,7 @@ Reading from URL from PIL import Image from urllib.request import urlopen - url = "https://python-pillow.org/assets/images/pillow-logo.png" + url = "https://python-pillow.github.io/assets/images/pillow-logo.png" img = Image.open(urlopen(url)) diff --git a/pyproject.toml b/pyproject.toml index 1c7b2c4b28e..aaaba0032e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ optional-dependencies.xmp = [ urls.Changelog = "/~https://github.com/python-pillow/Pillow/releases" urls.Documentation = "https://pillow.readthedocs.io" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" -urls.Homepage = "https://python-pillow.org" +urls.Homepage = "https://python-pillow.github.io" urls.Mastodon = "https://fosstodon.org/@pillow" urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls.Source = "/~https://github.com/python-pillow/Pillow" From 2215eaf21d8e9096fdd13c34225c67c0893d82db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Dec 2024 06:25:48 +1100 Subject: [PATCH 079/175] Updated openjpeg to 2.5.3 --- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_openjpeg.sh | 2 +- docs/installation/building-from-source.rst | 2 +- winbuild/build_prepare.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index ddda43423cd..e88a010ef5c 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -41,7 +41,7 @@ FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=10.0.1 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.0.4 -OPENJPEG_VERSION=2.5.2 +OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 8c2967bc21b..1f8d781931b 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,7 +1,7 @@ #!/bin/bash # install openjpeg -archive=openjpeg-2.5.2 +archive=openjpeg-2.5.3 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index ff60120aedc..03359de31cd 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -58,7 +58,7 @@ Many of Pillow's features require external libraries: * **openjpeg** provides JPEG 2000 functionality. * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, - **2.4.0**, **2.5.0** and **2.5.2**. + **2.4.0**, **2.5.0**, **2.5.2** and **2.5.3**. * Pillow does **not** support the earlier **1.5** series which ships with Debian Jessie. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c8332d11c7e..dc802a3e52f 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ def cmd_msbuild( "LCMS2": "2.16", "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", - "OPENJPEG": "2.5.2", + "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.3", "ZLIB": "1.3.1", From 522505b714085ca94a2cab2a71e77a4fc5df7a3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Dec 2024 17:52:34 +1100 Subject: [PATCH 080/175] Support saving CMYK JPEG2000 images --- Tests/test_file_jpeg2k.py | 12 ++++++++++++ src/libImaging/Jpeg2KEncode.c | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index fbf72ae0518..8bb290bf31d 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -325,6 +325,18 @@ def test_cmyk() -> None: assert im.getpixel((0, 0)) == (185, 134, 0, 0) +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@skip_unless_feature_version("jpg_2000", "2.5.3") +def test_cmyk_save() -> None: + with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2: + assert jp2.mode == "CMYK" + + im = roundtrip(jp2) + assert_image_equal(im, jp2) + + @pytest.mark.parametrize("ext", (".j2k", ".jp2")) def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index d30ccde603e..34d1a22949c 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -330,6 +330,13 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { components = 4; color_space = OPJ_CLRSPC_SRGB; pack = j2k_pack_rgba; +#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \ + (OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2) + } else if (strcmp(im->mode, "CMYK") == 0) { + components = 4; + color_space = OPJ_CLRSPC_CMYK; + pack = j2k_pack_rgba; +#endif } else { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; From 9ed96704d3788fa58d9682d293c4190e5da07c2d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Dec 2024 07:28:48 +1100 Subject: [PATCH 081/175] Do not attempt to install cmake if it is already present --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 9a9d1275f02..74a9795bc64 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 +Subproject commit 74a9795bc64ff786b7e7d33bdec2843cf17e512e From e361930936cd1208beeb292654a3af7548d863b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Dec 2024 18:56:59 +1100 Subject: [PATCH 082/175] Updated libjpeg-turbo to 3.1.0 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5f8a4cf78e4..86f51a586dc 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.44 -JPEGTURBO_VERSION=3.0.4 +JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1efe0dcac8b..fd104eb23f0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -113,7 +113,7 @@ def cmd_msbuild( "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "10.1.0", - "JPEGTURBO": "3.0.4", + "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBPNG": "1.6.44", "LIBWEBP": "1.4.0", @@ -155,7 +155,7 @@ def cmd_msbuild( cmd_copy("cjpeg-static.exe", "cjpeg.exe"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], - "headers": ["j*.h"], + "headers": ["jconfig.h", r"src\j*.h"], "libs": ["libjpeg.lib"], "bins": ["cjpeg.exe", "djpeg.exe"], }, From 642b44c3159077d0863c29181c8c6fbe60039e5b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Dec 2024 01:25:28 +1100 Subject: [PATCH 083/175] Test libjpeg-turbo on macOS --- .github/workflows/macos-install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index ddb4212301d..2301a3a7ef3 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -8,8 +8,8 @@ fi brew install \ freetype \ ghostscript \ + jpeg-turbo \ libimagequant \ - libjpeg \ libtiff \ little-cms2 \ openjpeg \ From 0e5f5fcb297b1e08ec83773562663b8ad9e53aee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 Dec 2024 16:33:20 +1100 Subject: [PATCH 084/175] Updated libXau to 1.0.12 --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 86f51a586dc..36036ff2685 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -105,7 +105,7 @@ function build { build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc From 62d8ccc015d68a9bb3da555f9800786652de4962 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Dec 2024 12:11:41 +1100 Subject: [PATCH 085/175] Derive dir from filename if root is the same --- winbuild/build_prepare.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5bdb5fafedb..2d804d55778 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -131,7 +131,6 @@ def cmd_msbuild( "libjpeg": { "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", - "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" @@ -161,7 +160,6 @@ def cmd_msbuild( "zlib": { "url": f"/~https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.zip", "filename": f"zlib-ng-{V['ZLIBNG']}.zip", - "dir": f"zlib-ng-{V['ZLIBNG']}", "license": "LICENSE.md", "patch": { r"CMakeLists.txt": { @@ -179,7 +177,6 @@ def cmd_msbuild( "xz": { "url": f"/~https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", "filename": f"xz-{V['XZ']}.tar.gz", - "dir": f"xz-{V['XZ']}", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -192,7 +189,6 @@ def cmd_msbuild( "libwebp": { "url": "http://downloads.webmproject.org/releases/webp/FILENAME", "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", - "dir": f"libwebp-{V['LIBWEBP']}", "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { @@ -214,7 +210,6 @@ def cmd_msbuild( "libtiff": { "url": "https://download.osgeo.org/libtiff/FILENAME", "filename": f"tiff-{V['TIFF']}.tar.gz", - "dir": f"tiff-{V['TIFF']}", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -247,7 +242,6 @@ def cmd_msbuild( "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" f"lpng{V['LIBPNG_DOTLESS']}.zip/download", "filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", - "dir": f"lpng{V['LIBPNG_DOTLESS']}", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), @@ -261,7 +255,6 @@ def cmd_msbuild( "brotli": { "url": f"/~https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz", "filename": f"brotli-{V['BROTLI']}.tar.gz", - "dir": f"brotli-{V['BROTLI']}", "license": "LICENSE", "build": [ *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -272,7 +265,6 @@ def cmd_msbuild( "freetype": { "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", "filename": f"freetype-{V['FREETYPE']}.tar.gz", - "dir": f"freetype-{V['FREETYPE']}", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -307,7 +299,6 @@ def cmd_msbuild( "lcms2": { "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", "filename": f"lcms2-{V['LCMS2']}.tar.gz", - "dir": f"lcms2-{V['LCMS2']}", "license": "LICENSE", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -333,7 +324,6 @@ def cmd_msbuild( "openjpeg": { "url": f"/~https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz", "filename": f"openjpeg-{V['OPENJPEG']}.tar.gz", - "dir": f"openjpeg-{V['OPENJPEG']}", "license": "LICENSE", "build": [ *cmds_cmake( @@ -348,7 +338,6 @@ def cmd_msbuild( # commit: Merge branch 'master' into msvc (matches 2.17.0 tag) "url": "/~https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", - "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab", "license": "COPYRIGHT", "patch": { "CMakeLists.txt": { @@ -368,7 +357,6 @@ def cmd_msbuild( "harfbuzz": { "url": f"/~https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", - "dir": f"harfbuzz-{V['HARFBUZZ']}", "license": "COPYING", "build": [ *cmds_cmake( @@ -383,7 +371,6 @@ def cmd_msbuild( "fribidi": { "url": f"/~https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip", "filename": f"fribidi-{V['FRIBIDI']}.zip", - "dir": f"fribidi-{V['FRIBIDI']}", "license": "COPYING", "build": [ cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"), @@ -763,6 +750,8 @@ def main() -> None: } for k, v in DEPS.items(): + if "dir" not in v: + v["dir"] = re.sub(r"\.(tar\.gz|zip)", "", v["filename"]) prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) print() From 6373e8bcc470ffa8849a17fa8e90e1146e0c3c6c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Dec 2024 22:58:35 +1100 Subject: [PATCH 086/175] Use .tar.gz zlib to match macOS and Linux --- winbuild/build_prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 5bdb5fafedb..a68a0b132b2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -159,8 +159,8 @@ def cmd_msbuild( "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": f"/~https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.zip", - "filename": f"zlib-ng-{V['ZLIBNG']}.zip", + "url": f"/~https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.tar.gz", + "filename": f"zlib-ng-{V['ZLIBNG']}.tar.gz", "dir": f"zlib-ng-{V['ZLIBNG']}", "license": "LICENSE.md", "patch": { From 7f6ebfb8c597d8d1a074993f463992b6dac34c41 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Dec 2024 23:40:38 +1100 Subject: [PATCH 087/175] Added release notes for #8500 --- docs/releasenotes/11.1.0.rst | 59 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 60 insertions(+) create mode 100644 docs/releasenotes/11.1.0.rst diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst new file mode 100644 index 00000000000..c5d0afd58d8 --- /dev/null +++ b/docs/releasenotes/11.1.0.rst @@ -0,0 +1,59 @@ +11.1.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Check for zlib-ng +^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the zlib-ng version of the +zlib library, and what version of zlib-ng is being used:: + + from PIL import features + features.check_feature("zlib_ng") # True or False + features.version_feature("zlib_ng") # "2.2.2" for example, or None + +Other Changes +============= + +zlib-ng in wheels +^^^^^^^^^^^^^^^^^ + +Wheels are now built against zlib-ng for improved speed. In tests, saving a PNG +was found to be more than twice as fast at higher compression levels. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 641cda4efb5..bd8e5536f71 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 + 11.1.0 11.0.0 10.4.0 10.3.0 From 5e35ca359c4182af07c0c02e61ebd8d270bb34b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Dec 2024 20:17:36 +1100 Subject: [PATCH 088/175] Updated libjpeg-turbo URL --- 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 a68a0b132b2..e702c7014d7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -129,7 +129,7 @@ def cmd_msbuild( # dependencies, listed in order of compilation DEPS: dict[str, dict[str, Any]] = { "libjpeg": { - "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download", + "url": f"/~https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/{V['JPEGTURBO']}/libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], From 095811452172bc4efd4be172f850c2f564e35cba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Dec 2024 21:03:33 +1100 Subject: [PATCH 089/175] Corrected harfbuzz URL --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index de2471c3f30..3e2a14462dc 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -93,7 +93,7 @@ function build_harfbuzz { if [ -e harfbuzz-stamp ]; then return; fi python3 -m pip install meson ninja - local out_dir=$(fetch_unpack /~https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + local out_dir=$(fetch_unpack /~https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ From 962bfc6fd5e949cfdf8c0d18877acc2b8f82a78c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Dec 2024 10:39:52 +1100 Subject: [PATCH 090/175] Updated libjpeg-turbo URL --- wheels/multibuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels/multibuild b/wheels/multibuild index 74a9795bc64..42d761728d1 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 74a9795bc64ff786b7e7d33bdec2843cf17e512e +Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155 From de8335ba8fe121cf1f1b7b1565f535fb066094bf Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:07:34 +1100 Subject: [PATCH 091/175] Extract tar files with "data" filter in Windows build scripts (#8606) Co-authored-by: Andrew Murray Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- winbuild/build_prepare.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 607f672ba9a..188872dfc69 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import shutil import struct import subprocess +import sys from typing import Any @@ -507,7 +508,10 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: if sources_dir_abs != member_prefix: msg = "Attempted Path Traversal in Tar File" raise RuntimeError(msg) - tgz.extractall(sources_dir) + if sys.version_info >= (3, 12): + tgz.extractall(sources_dir, filter="data") + else: + tgz.extractall(sources_dir) else: msg = "Unknown archive type: " + filename raise RuntimeError(msg) From cbc55c4621b79176702bf14f188e5a439c7804a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:08:51 +1100 Subject: [PATCH 092/175] Raise ValueError when WMF inch is zero (#8600) Co-authored-by: Andrew Murray --- Tests/test_file_wmf.py | 7 +++++++ src/PIL/WmfImagePlugin.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 424640d7b18..2f1f8cdbc85 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -35,6 +35,13 @@ def test_load() -> None: assert im.load()[0, 0] == (255, 255, 255) +def test_load_zero_inch() -> None: + b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x00" * 10) + with pytest.raises(ValueError): + with Image.open(b): + pass + + def test_register_handler(tmp_path: Path) -> None: class TestHandler(ImageFile.StubHandler): methodCalled = False diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index cad6c98d53f..48e9823e8ad 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -92,6 +92,9 @@ def _open(self) -> None: # get units per inch self._inch = word(s, 14) + if self._inch == 0: + msg = "Invalid inch" + raise ValueError(msg) # get bounding box x0 = short(s, 6) From c6f42cb6a59aaaf29acf8831ef5865a688139480 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 Dec 2024 14:46:01 +1100 Subject: [PATCH 093/175] Updated libwebp to 1.5.0 --- .github/workflows/wheels-dependencies.sh | 2 +- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3e2a14462dc..4e0fad79f4e 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -51,7 +51,7 @@ else GIFLIB_VERSION=5.2.1 fi ZLIB_NG_VERSION=2.2.2 -LIBWEBP_VERSION=1.4.0 +LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 diff --git a/depends/install_webp.sh b/depends/install_webp.sh index c47fb35f125..9d29777159e 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.4.0 +archive=libwebp-1.5.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 188872dfc69..0674a9a1528 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -117,7 +117,7 @@ def cmd_msbuild( "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBPNG": "1.6.44", - "LIBWEBP": "1.4.0", + "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.3", From 5bd2f489fa7ed1ca4097cf15634d37723919434d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:44:50 +1100 Subject: [PATCH 094/175] Install libjpeg-turbo8-dev (#8610) Co-authored-by: Andrew Murray --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index e85e6bdc575..5c20e7f3727 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -21,7 +21,7 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ - ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ + ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ sway wl-clipboard libopenblas-dev fi From cf4110ff06b4d4e0ec43b4a9ca13a28b7d3b11d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 21 Dec 2024 00:55:44 +1100 Subject: [PATCH 095/175] Replace constants with enums (#8611) Co-authored-by: Andrew Murray --- 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 1e289b6c38b..440728b08f2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1565,7 +1565,7 @@ def get_child_images(self) -> list[ImageFile.ImageFile]: for subifd_offset in subifd_offsets: ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) - if ifd1 and ifd1.get(513): + if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): assert exif._info is not None ifds.append((ifd1, exif._info.next)) @@ -1577,11 +1577,11 @@ def get_child_images(self) -> list[ImageFile.ImageFile]: fp = self.fp if ifd is not None: - thumbnail_offset = ifd.get(513) + thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) if thumbnail_offset is not None: thumbnail_offset += getattr(self, "_exif_offset", 0) self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(514)) + data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount)) fp = io.BytesIO(data) with open(fp) as im: From 3d0f4389499314ca793f6244d13e00cd3ad44de0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:38:05 +0000 Subject: [PATCH 096/175] Update dependency mypy to v1.14.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index c84a3533b85..cd1b1a1a1dc 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.13.0 +mypy==1.14.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 08e1f9ebc11285c94a2706dd02146f685b10aec0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 Dec 2024 13:04:17 +1100 Subject: [PATCH 097/175] Lint fixes --- Tests/test_color_lut.py | 2 ++ src/PIL/ImageFilter.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 36ab187f261..baa899df5df 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -388,10 +388,12 @@ def test_numpy_sources(self) -> None: table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) assert lut.table.shape == (table.size,) table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) assert lut.table.shape == (table.size,) # Check application diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 8b0974b2c37..b350e56f4f8 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -553,7 +553,7 @@ def transform( ch_out = channels or ch_in size_1d, size_2d, size_3d = self.size - table = [0] * (size_1d * size_2d * size_3d * ch_out) + table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out) idx_in = 0 idx_out = 0 for b in range(size_3d): From b99a00f44f8e34c18b89395eaf67e796ecdf1bf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Dec 2024 07:24:08 +1100 Subject: [PATCH 098/175] Updated MakerNote IFD capitalization --- src/PIL/ExifTags.py | 2 +- src/PIL/Image.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 39b4aa55262..231e80f4080 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -352,7 +352,7 @@ class Interop(IntEnum): class IFD(IntEnum): Exif = 34665 GPSInfo = 34853 - Makernote = 37500 + MakerNote = 37500 Interop = 40965 IFD1 = -1 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1e289b6c38b..fb3f01fc8b8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3884,7 +3884,7 @@ class Exif(_ExifBase): gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) print(gps_ifd) - Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``, ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: @@ -4047,11 +4047,11 @@ def get_ifd(self, tag: int) -> dict[int, Any]: ifd = self._get_ifd_dict(offset, tag) if ifd is not None: self._ifds[tag] = ifd - elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]: if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) tag_data = self._ifds[ExifTags.IFD.Exif][tag] - if tag == ExifTags.IFD.Makernote: + if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 if tag_data[:8] == b"FUJIFILM": @@ -4138,7 +4138,7 @@ def get_ifd(self, tag: int) -> dict[int, Any]: ifd = { k: v for (k, v) in ifd.items() - if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) + if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) } return ifd From 5fe80e1c48eff0b74a14377107895c3e2d2f91b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Dec 2024 07:30:59 +1100 Subject: [PATCH 099/175] Use hex values more consistently --- src/PIL/ExifTags.py | 124 ++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 231e80f4080..207d4de4e24 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -303,38 +303,38 @@ class Base(IntEnum): class GPS(IntEnum): - GPSVersionID = 0 - GPSLatitudeRef = 1 - GPSLatitude = 2 - GPSLongitudeRef = 3 - GPSLongitude = 4 - GPSAltitudeRef = 5 - GPSAltitude = 6 - GPSTimeStamp = 7 - GPSSatellites = 8 - GPSStatus = 9 - GPSMeasureMode = 10 - GPSDOP = 11 - GPSSpeedRef = 12 - GPSSpeed = 13 - GPSTrackRef = 14 - GPSTrack = 15 - GPSImgDirectionRef = 16 - GPSImgDirection = 17 - GPSMapDatum = 18 - GPSDestLatitudeRef = 19 - GPSDestLatitude = 20 - GPSDestLongitudeRef = 21 - GPSDestLongitude = 22 - GPSDestBearingRef = 23 - GPSDestBearing = 24 - GPSDestDistanceRef = 25 - GPSDestDistance = 26 - GPSProcessingMethod = 27 - GPSAreaInformation = 28 - GPSDateStamp = 29 - GPSDifferential = 30 - GPSHPositioningError = 31 + GPSVersionID = 0x00 + GPSLatitudeRef = 0x01 + GPSLatitude = 0x02 + GPSLongitudeRef = 0x03 + GPSLongitude = 0x04 + GPSAltitudeRef = 0x05 + GPSAltitude = 0x06 + GPSTimeStamp = 0x07 + GPSSatellites = 0x08 + GPSStatus = 0x09 + GPSMeasureMode = 0x0A + GPSDOP = 0x0B + GPSSpeedRef = 0x0C + GPSSpeed = 0x0D + GPSTrackRef = 0x0E + GPSTrack = 0x0F + GPSImgDirectionRef = 0x10 + GPSImgDirection = 0x11 + GPSMapDatum = 0x12 + GPSDestLatitudeRef = 0x13 + GPSDestLatitude = 0x14 + GPSDestLongitudeRef = 0x15 + GPSDestLongitude = 0x16 + GPSDestBearingRef = 0x17 + GPSDestBearing = 0x18 + GPSDestDistanceRef = 0x19 + GPSDestDistance = 0x1A + GPSProcessingMethod = 0x1B + GPSAreaInformation = 0x1C + GPSDateStamp = 0x1D + GPSDifferential = 0x1E + GPSHPositioningError = 0x1F """Maps EXIF GPS tags to tag names.""" @@ -342,40 +342,40 @@ class GPS(IntEnum): class Interop(IntEnum): - InteropIndex = 1 - InteropVersion = 2 - RelatedImageFileFormat = 4096 - RelatedImageWidth = 4097 - RelatedImageHeight = 4098 + InteropIndex = 0x0001 + InteropVersion = 0x0002 + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageHeight = 0x1002 class IFD(IntEnum): - Exif = 34665 - GPSInfo = 34853 - MakerNote = 37500 - Interop = 40965 + Exif = 0x8769 + GPSInfo = 0x8825 + MakerNote = 0x927C + Interop = 0xA005 IFD1 = -1 class LightSource(IntEnum): - Unknown = 0 - Daylight = 1 - Fluorescent = 2 - Tungsten = 3 - Flash = 4 - Fine = 9 - Cloudy = 10 - Shade = 11 - DaylightFluorescent = 12 - DayWhiteFluorescent = 13 - CoolWhiteFluorescent = 14 - WhiteFluorescent = 15 - StandardLightA = 17 - StandardLightB = 18 - StandardLightC = 19 - D55 = 20 - D65 = 21 - D75 = 22 - D50 = 23 - ISO = 24 - Other = 255 + Unknown = 0x00 + Daylight = 0x01 + Fluorescent = 0x02 + Tungsten = 0x03 + Flash = 0x04 + Fine = 0x09 + Cloudy = 0x0A + Shade = 0x0B + DaylightFluorescent = 0x0C + DayWhiteFluorescent = 0x0D + CoolWhiteFluorescent = 0x0E + WhiteFluorescent = 0x0F + StandardLightA = 0x11 + StandardLightB = 0x12 + StandardLightC = 0x13 + D55 = 0x14 + D65 = 0x15 + D75 = 0x16 + D50 = 0x17 + ISO = 0x18 + Other = 0xFF From fb3d80e390b8ed3111a4658755b035f5e20eb5e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 24 Dec 2024 00:41:27 +1100 Subject: [PATCH 100/175] Fixed connecting discontiguous corners --- .../imagedraw/discontiguous_corners_polygon.png | Bin 486 -> 533 bytes Tests/test_imagedraw.py | 3 +++ src/libImaging/Draw.c | 9 ++++----- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png index 509c42b26e0cbc5e01853915b083d367c1587579..1b58889c8f3ae45243a7509c907f1928534bcbde 100644 GIT binary patch delta 507 zcmV$=WdTX_c%swU!U485w>wyG`7FR7NL{=~F7gqE=@exLJl^>&(VxOp^oSf8ia zhL}fVLnf5pHi+D7J^>rS>@}Z&jbnD1M`NRzjpotVIm~MFXn*Xdtc%T~u_cKYm`7u4 zb5Aml#`f$rpm{%7q?w2WMdmZ9Jmv$tYM1zlSN+e25z{{|T< xlUzDe$vZP-&cnLR`Fl4@D(PjuQYa0yf8_cULF8KV4%i50t$7D*9J9(i8XL_lG>^viVP>00V}E-xGt8s0{h7DSqp_oy z7v|B}vCOaL^=6`p{ zx-yW@%sX@;oL`>6ss&}tQP{jy7s{L0??ngmnl7|5FWZZ5 z<^^5oX`ZzgZGX-0ccHoYoxSL9e%Xan%un{>AoFjtaGZJeq=YAkoRJ&<-`JdWJ^yZh zPW{aTo}5yhZ}4mL8Pdz|GqL&VQ?P{M?R+?oa?YPd*|&W-o5uFwQh3{kbINRgm&E+= z{-We$^*vu@&WF;P59Bw=Z25$j-}aE!{@K0+IYFQ86GhJXGa>d@T{hc8HjiI*d2A1< zc9ZEp%#(a~rkpp^WX{df`g}hP-I2X&ex`d$_dGxT#Oc95ub{>N0000 None: def test_discontiguous_corners_polygon() -> None: img, draw = create_base_image_draw((84, 68)) draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) + draw.polygon( + ((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), BLACK + ) draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) draw.polygon( ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index f1c8ffcff8d..ea6f8805efb 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -501,7 +501,8 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if (current->dx != 0 && roundf(xx[j - 1]) == xx[j - 1]) { + } else if (current->dx != 0 && j % 2 == 1 && + roundf(xx[j - 1]) == xx[j - 1]) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; @@ -510,10 +511,8 @@ polygon_generic( continue; } // Check if the two edges join to make a corner - if (((ymin == current->ymin && ymin == other_edge->ymin) || - (ymin == current->ymax && ymin == other_edge->ymax)) && - xx[j - 1] == (ymin - other_edge->y0) * other_edge->dx + - other_edge->x0) { + if (xx[j - 1] == + (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) { // Determine points from the edges on the next row // Or if this is the last row, check the previous row int offset = ymin == ymax ? -1 : 1; From dd410e4b32c6b7e412304f30c27b8a226c24ebd3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Dec 2024 10:51:45 +1100 Subject: [PATCH 101/175] Added reading of J2K comments --- Tests/test_file_jpeg2k.py | 5 +++-- src/PIL/Jpeg2KImagePlugin.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index fbf72ae0518..34176d3cecb 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -424,8 +424,9 @@ def test_pclr() -> None: def test_comment() -> None: - with Image.open("Tests/images/comment.jp2") as im: - assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" + for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"): + with Image.open(path) as im: + assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" # Test an image that is truncated partway through a codestream with open("Tests/images/comment.jp2", "rb") as fp: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index b6ebd562be6..67828358d37 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -252,6 +252,7 @@ def _open(self) -> None: if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" self._size, self._mode = _parse_codestream(self.fp) + self._parse_comment() else: sig = sig + self.fp.read(8) @@ -262,6 +263,9 @@ def _open(self) -> None: if dpi is not None: self.info["dpi"] = dpi if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + hdr = self.fp.read(2) + length = _binary.i16be(hdr) + self.fp.seek(length - 2, os.SEEK_CUR) self._parse_comment() else: msg = "not a JPEG 2000 file" @@ -296,10 +300,6 @@ def _open(self) -> None: ] def _parse_comment(self) -> None: - hdr = self.fp.read(2) - length = _binary.i16be(hdr) - self.fp.seek(length - 2, os.SEEK_CUR) - while True: marker = self.fp.read(2) if not marker: From 62b7cb62f4ee339677170b0d0fa5943e490ddab7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Dec 2024 19:06:23 +1100 Subject: [PATCH 102/175] Fixed indentation --- src/_imagingcms.c | 56 +++++++++++++++++++++++------------------------ src/display.c | 14 +++++++----- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 1823bcf0371..1805ebde17f 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -346,10 +346,10 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { return -1; } - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - // transform color channels only - for (i = 0; i < im->ysize; i++) { + // transform color channels only + for (i = 0; i < im->ysize; i++) { cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); } @@ -362,9 +362,9 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { // enough available on all platforms, so we polyfill it here for now. pyCMScopyAux(hTransform, imOut, im); - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - return 0; + return 0; } static cmsHTRANSFORM @@ -378,17 +378,17 @@ _buildTransform( ) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - /* create the transform */ - hTransform = cmsCreateTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - iRenderingIntent, - cmsFLAGS - ); + /* create the transform */ + hTransform = cmsCreateTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + iRenderingIntent, + cmsFLAGS + ); Py_END_ALLOW_THREADS; @@ -412,19 +412,19 @@ _buildProofTransform( ) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS - - /* create the transform */ - hTransform = cmsCreateProofingTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - hProofProfile, - iRenderingIntent, - iProofIntent, - cmsFLAGS - ); + Py_BEGIN_ALLOW_THREADS; + + /* create the transform */ + hTransform = cmsCreateProofingTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + hProofProfile, + iRenderingIntent, + iProofIntent, + cmsFLAGS + ); Py_END_ALLOW_THREADS; diff --git a/src/display.c b/src/display.c index b4e2e38991a..eed75975d71 100644 --- a/src/display.c +++ b/src/display.c @@ -690,24 +690,26 @@ PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { SetWindowLongPtr(wnd, 0, (LONG_PTR)callback); SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR)PyThreadState_Get()); - Py_BEGIN_ALLOW_THREADS ShowWindow(wnd, SW_SHOWNORMAL); + Py_BEGIN_ALLOW_THREADS; + ShowWindow(wnd, SW_SHOWNORMAL); SetForegroundWindow(wnd); /* to make sure it's visible */ - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - return Py_BuildValue(F_HANDLE, wnd); + return Py_BuildValue(F_HANDLE, wnd); } PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { MSG msg; - Py_BEGIN_ALLOW_THREADS while (mainloop && GetMessage(&msg, NULL, 0, 0)) { + Py_BEGIN_ALLOW_THREADS; + while (mainloop && GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - Py_INCREF(Py_None); + Py_INCREF(Py_None); return Py_None; } From 622722f295ab137ea6bf54ede8efb8f376d1194b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 26 Dec 2024 20:04:27 +1100 Subject: [PATCH 103/175] Corrected loadImageSeries type hint --- src/PIL/SpiderImagePlugin.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 075073f9fe3..c83d02885ff 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -211,26 +211,27 @@ def tkPhotoImage(self) -> ImageTk.PhotoImage: # given a list of filenames, return a list of images -def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: +def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None: """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: return None - imglist = [] + byte_imgs = [] for img in filelist: if not os.path.exists(img): print(f"unable to find {img}") continue try: with Image.open(img) as im: - im = im.convert2byte() + assert isinstance(im, SpiderImageFile) + byte_im = im.convert2byte() except Exception: if not isSpiderImage(img): print(f"{img} is not a Spider image file") continue - im.info["filename"] = img - imglist.append(im) - return imglist + byte_im.info["filename"] = img + byte_imgs.append(byte_im) + return byte_imgs # -------------------------------------------------------------------- From aef3aa2ab35fdf847aa13017c5b4b315562a1f77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:26:07 +1100 Subject: [PATCH 104/175] Pass file handle to ContainerIO --- Tests/test_file_container.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 237045acc7b..597ab508342 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -4,8 +4,6 @@ from PIL import ContainerIO, Image -from .helper import hopper - TEST_FILE = "Tests/images/dummy.container" @@ -15,15 +13,15 @@ def test_sanity() -> None: def test_isatty() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.isatty() is False def test_seekable() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.seekable() is True From 0148684c2412cc69b9606e7e2b6d5d581030e41c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:29:47 +1100 Subject: [PATCH 105/175] Use monkeypatch --- Tests/test_image_thumbnail.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 01bd4b1d76b..aa625f22960 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -104,20 +104,20 @@ def test_transposed() -> None: assert im.size == (590, 88) -def test_load_first_unless_jpeg() -> None: +def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: - draft = im.draft + original_draft = im.draft def im_draft( mode: str, size: tuple[int, int] ) -> tuple[str, tuple[int, int, float, float]] | None: - result = draft(mode, size) + result = original_draft(mode, size) assert result is not None return result - im.draft = im_draft + monkeypatch.setattr(im, "draft", im_draft) im.thumbnail((64, 64)) From 89f1498796be0053bc40ef757680c9f7bc49e7ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 27 Dec 2024 11:38:47 +1100 Subject: [PATCH 106/175] Updated argument types to match Image draft --- Tests/test_image_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index aa625f22960..1181f6fcaca 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -110,7 +110,7 @@ def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: original_draft = im.draft def im_draft( - mode: str, size: tuple[int, int] + mode: str | None, size: tuple[int, int] | None ) -> tuple[str, tuple[int, int, float, float]] | None: result = original_draft(mode, size) assert result is not None From 0220b025c5f56ebe4c2f1678e24195672d4d4373 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 12:33:59 +1100 Subject: [PATCH 107/175] Updated documentation for #7947 and #8592 --- docs/handbook/image-file-formats.rst | 8 +++++--- docs/releasenotes/11.1.0.rst | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bf3087f6f68..364e1802ae5 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -573,9 +573,11 @@ 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). +``RGBA``, and ``YCbCr`` images with subsampled components. Pillow 10.4.0 and +later can read ``CMYK`` images with OpenJPEG 2.5.1 and later, and Pillow 11.1.0 +and later can write ``CMYK`` images with OpenJPEG 2.5.3 and later. 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 diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58d8..7fd622beb30 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -52,6 +52,11 @@ zlib library, and what version of zlib-ng is being used:: Other Changes ============= +Saving JPEG 2000 CMYK images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files. + zlib-ng in wheels ^^^^^^^^^^^^^^^^^ From ad747f3fd8024145ddc8dbc5f3f95d7e396b3351 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 12:38:50 +1100 Subject: [PATCH 108/175] Added release notes --- docs/releasenotes/11.1.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58d8..cccf2323df5 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -52,6 +52,12 @@ zlib library, and what version of zlib-ng is being used:: Other Changes ============= +Reading JPEG 2000 comments +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG 2000 image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images. + zlib-ng in wheels ^^^^^^^^^^^^^^^^^ From cfe8379d905bb92a1407d1809987fb74163b5024 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 13:02:23 +1100 Subject: [PATCH 109/175] Added release notes for #8483 --- docs/releasenotes/11.1.0.rst | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58d8..90a0492bd71 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -31,10 +31,26 @@ TODO API Changes =========== -TODO -^^^^ +Writing XMP bytes to JPEG and MPO +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Pillow 11.0.0 added writing XMP data to JPEG and MPO images:: + + im.info["xmp"] = b"test" + im.save("out.jpg") + +However, this meant that XMP data was automatically kept from an opened image, +which is inconsistent with the rest of Pillow's behaviour. This functionality +has been removed. To write XMP data, the ``xmp`` argument can still be used for +JPEG files:: + + im.save("out.jpg", xmp=b"test") + +To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now +be used:: + + second_im.encoderinfo = {"xmp": b"test"} + im.save("out.mpo", save_all=True, append_images=[second_im]) API Additions ============= From 23083f28abbf0a79bc44c2e7b755663c04368e14 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 14:02:19 +1100 Subject: [PATCH 110/175] Use monkeypatch --- Tests/test_file_png.py | 8 ++------ Tests/test_file_ppm.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ffafc3c582a..974e1e75faa 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -772,22 +772,18 @@ def test_seek(self) -> None: im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer: bool) -> None: - old_stdout = sys.stdout + def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: class MyStdOut: buffer = BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_PNG_FILE) as im: im.save(sys.stdout, "PNG") - # Reset stdout - sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fb08d613a56..ee51a5e5a6b 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -367,22 +367,18 @@ def test_mimetypes(tmp_path: Path) -> None: @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer: bool) -> None: - old_stdout = sys.stdout +def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: class MyStdOut: buffer = BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() - sys.stdout = mystdout + monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_FILE) as im: im.save(sys.stdout, "PPM") - # Reset stdout - sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer with Image.open(mystdout) as reloaded: From f10e9f42d3e434c34a16df514a5435381b21aefb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 14:29:29 +1100 Subject: [PATCH 111/175] Do not use temporary file in grabclipboard() on macOS --- src/PIL/ImageGrab.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e27ca7e5033..fe27bfaeb6f 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -104,28 +104,17 @@ def grab( def grabclipboard() -> Image.Image | list[str] | None: if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".png") - os.close(fh) - commands = [ - 'set theFile to (open for access POSIX file "' - + filepath - + '" with write permission)', - "try", - " write (the clipboard as «class PNGf») to theFile", - "end try", - "close access theFile", - ] - script = ["osascript"] - for command in commands: - script += ["-e", command] - subprocess.call(script) + p = subprocess.run( + ["osascript", "-e", "get the clipboard as «class PNGf»"], + capture_output=True, + ) + if p.returncode != 0: + return None - im = None - if os.stat(filepath).st_size != 0: - im = Image.open(filepath) - im.load() - os.unlink(filepath) - return im + import binascii + + data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3])) + return Image.open(data) elif sys.platform == "win32": fmt, data = Image.core.grabclipboard_win32() if fmt == "file": # CF_HDROP From 05c981ffd74f6b4de932a1a14f44dc6a3058bb75 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 15:41:19 +1100 Subject: [PATCH 112/175] Removed buffer_size variable --- src/libImaging/Jpeg2KDecode.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index fc927d2f0c0..4f185b529ac 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -640,7 +640,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { opj_dparameters_t params; OPJ_COLOR_SPACE color_space; j2k_unpacker_t unpack = NULL; - size_t buffer_size = 0, tile_bytes = 0; + size_t tile_bytes = 0; unsigned n, tile_height, tile_width; int subsampling; int total_component_width = 0; @@ -870,7 +870,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { tile_info.data_size = tile_bytes; } - if (buffer_size < tile_info.data_size) { + if (tile_info.data_size > 0) { /* malloc check ok, overflow and tile size sanity check above */ UINT8 *new = realloc(state->buffer, tile_info.data_size); if (!new) { @@ -883,7 +883,6 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { to valgrind errors. */ memset(new, 0, tile_info.data_size); state->buffer = new; - buffer_size = tile_info.data_size; } if (!opj_decode_tile_data( From 8945875c6ce48fee62d17cea1414938e24808a9f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 21:28:44 +1100 Subject: [PATCH 113/175] Populate DPI from JFIF cm density --- Tests/images/jfif_unit_cm.jpg | Bin 0 -> 391 bytes Tests/test_file_jpeg.py | 4 ++++ src/PIL/JpegImagePlugin.py | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 Tests/images/jfif_unit_cm.jpg diff --git a/Tests/images/jfif_unit_cm.jpg b/Tests/images/jfif_unit_cm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..78b50e60a236bf320a29c433eae2c9afeb759fe9 GIT binary patch literal 391 zcmex=5D>Bm<7<_#hv=|r|IDm3|AQ=W8AeI7R zhP2G0;u4@jeu%>2)a3lU6o&t|8Jrn7*x1UW@aW9W>!`f7NBe`P@aKBkX1<0(2-3z zFp*uUP{gQl;zAB(r;P_igD!qhF-|IK;^Yz&myncFRa4i{)G{$OGqmaka3 zYSZQ|TeofBv2)j tuple[int, int] | None: assert test(100, 200) == (100, 200) assert test(0) is None # square pixels + def test_dpi_jfif_cm(self): + with Image.open("Tests/images/jfif_unit_cm.jpg") as im: + assert im.info["dpi"] == (2.54, 5.08) + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 5025f88ea97..457690aac51 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -90,6 +90,9 @@ def APP(self: JpegImageFile, marker: int) -> None: else: if jfif_unit == 1: self.info["dpi"] = jfif_density + elif jfif_unit == 2: # cm + # 1 dpcm = 2.54 dpi + self.info["dpi"] = tuple(d * 2.54 for d in jfif_density) self.info["jfif_unit"] = jfif_unit self.info["jfif_density"] = jfif_density elif marker == 0xFFE1 and s[:6] == b"Exif\0\0": From 9bebecf36d66b19ac7ba0241ef9eb7febdcaf866 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Dec 2024 22:18:02 +1100 Subject: [PATCH 114/175] Use versionadded --- docs/handbook/image-file-formats.rst | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 364e1802ae5..2ea49282ecf 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -572,12 +572,19 @@ JPEG 2000 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 10.4.0 and -later can read ``CMYK`` images with OpenJPEG 2.5.1 and later, and Pillow 11.1.0 -and later can write ``CMYK`` images with OpenJPEG 2.5.3 and later. Pillow -supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 -files (``.jp2`` or ``.jpx`` files). + +.. versionadded:: 8.3.0 + Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with + subsampled components. + +.. versionadded:: 10.4.0 + Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later. + +.. versionadded:: 11.1.0 + Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later. + +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 9368a86397a41817f671c3c0bce7b8745bc5e218 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Dec 2024 07:43:47 +1100 Subject: [PATCH 115/175] Keep new IFDs when converting EXIF to bytes --- Tests/test_image.py | 4 ++++ src/PIL/Image.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index c8df474f493..092bc07f6f1 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -793,6 +793,10 @@ def test_empty_get_ifd(self) -> None: ifd[36864] = b"0220" assert exif.get_ifd(0x8769) == {36864: b"0220"} + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"} + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 90374d80469..dff3d063b13 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -4023,6 +4023,9 @@ def tobytes(self, offset: int = 8) -> bytes: head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) + for tag, ifd_dict in self._ifds.items(): + if tag not in self: + ifd[tag] = ifd_dict for tag, value in self.items(): if tag in [ ExifTags.IFD.Exif, From ea962bf1d8dab61d526f885eccb34863ea85228f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Dec 2024 16:59:32 +1100 Subject: [PATCH 116/175] Added RGBX;16N to RGB unpacker --- src/libImaging/Unpack.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index c23d5d889f6..e9203fe4d74 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1695,6 +1695,7 @@ static struct { #ifdef WORDS_BIGENDIAN {"RGB", "RGB;16N", 48, unpackRGB16B}, + {"RGB", "RGBX;16N", 64, unpackRGBA16B}, {"RGBA", "RGBa;16N", 64, unpackRGBa16B}, {"RGBA", "RGBA;16N", 64, unpackRGBA16B}, {"RGBX", "RGBX;16N", 64, unpackRGBA16B}, @@ -1708,6 +1709,7 @@ static struct { {"RGBA", "A;16N", 16, band316B}, #else {"RGB", "RGB;16N", 48, unpackRGB16L}, + {"RGB", "RGBX;16N", 64, unpackRGBA16L}, {"RGBA", "RGBa;16N", 64, unpackRGBa16L}, {"RGBA", "RGBA;16N", 64, unpackRGBA16L}, {"RGBX", "RGBX;16N", 64, unpackRGBA16L}, From 8d28514e409bf3ecbeb3721d8ccb508c09f2b975 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:16:42 +0200 Subject: [PATCH 117/175] Add zizmor to pre-commit and fix potential cache-poisoning in wheels workflow --- .github/workflows/wheels.yml | 2 -- .pre-commit-config.yaml | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c5e55aa621d..3b22ee98a2c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -263,8 +263,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.x" - cache: pip - cache-dependency-path: "Makefile" - run: make sdist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f91260c724f..b76f92ec00e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: /~https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + rev: v0.8.4 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: /~https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.4 + rev: v19.1.5 hooks: - id: clang-format types: [c] @@ -56,6 +56,11 @@ repos: - id: check-readthedocs - id: check-renovate + - repo: /~https://github.com/woodruffw/zizmor-pre-commit + rev: v0.10.0 + hooks: + - id: zizmor + - repo: /~https://github.com/sphinx-contrib/sphinx-lint rev: v1.0.0 hooks: From 167ed55d8b43de26c5ce01c239ce848062e5e995 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Dec 2024 19:37:38 +1100 Subject: [PATCH 118/175] Use elif --- src/PIL/TiffImagePlugin.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 16c521bea25..2ab0b7ebe68 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1559,17 +1559,6 @@ def _setup(self) -> None: # fillorder==2 modes have a corresponding # fillorder=1 mode self._mode, rawmode = OPEN_INFO[key] - # libtiff always returns the bytes in native order. - # we're expecting image byte order. So, if the rawmode - # contains I;16, we need to convert from native to image - # byte order. - if rawmode == "I;16": - rawmode = "I;16N" - if ";16B" in rawmode: - rawmode = rawmode.replace(";16B", ";16N") - if ";16L" in rawmode: - rawmode = rawmode.replace(";16L", ";16N") - # YCbCr images with new jpeg compression with pixels in one plane # unpacked straight into RGB values if ( @@ -1578,6 +1567,16 @@ def _setup(self) -> None: and self._planar_configuration == 1 ): rawmode = "RGB" + # libtiff always returns the bytes in native order. + # we're expecting image byte order. So, if the rawmode + # contains I;16, we need to convert from native to image + # byte order. + elif rawmode == "I;16": + rawmode = "I;16N" + elif ";16B" in rawmode: + rawmode = rawmode.replace(";16B", ";16N") + elif ";16L" in rawmode: + rawmode = rawmode.replace(";16L", ";16N") # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds From 7cee64ad1b1cbd558cdb01edaa9444f60467947b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Dec 2024 19:45:46 +1100 Subject: [PATCH 119/175] Use endswith --- src/PIL/TiffImagePlugin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2ab0b7ebe68..ab760c8fbd6 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1573,10 +1573,8 @@ def _setup(self) -> None: # byte order. elif rawmode == "I;16": rawmode = "I;16N" - elif ";16B" in rawmode: - rawmode = rawmode.replace(";16B", ";16N") - elif ";16L" in rawmode: - rawmode = rawmode.replace(";16L", ";16N") + elif rawmode.endswith(";16B") or rawmode.endswith(";16L"): + rawmode = rawmode[:-1] + "N" # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds From 050caa9cae5a5844e934e7ec29c0c5bc42537e32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 30 Dec 2024 21:14:23 +1100 Subject: [PATCH 120/175] Restored Makernote as a deprecated enum --- docs/deprecations.rst | 8 ++++++++ docs/releasenotes/11.1.0.rst | 7 ++++--- src/PIL/ExifTags.py | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 25607e27c3b..80966ca362a 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -175,6 +175,14 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt raw pointers to ``ImagingCore`` internals. To interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule`` object. +ExifTags.IFD.Makernote +^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.1.0 + +``ExifTags.IFD.Makernote`` has been deprecated. Instead, use +``ExifTags.IFD.MakerNote``. + Removed features ---------------- diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index c5d0afd58d8..57a8eef4069 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -23,10 +23,11 @@ TODO Deprecations ============ -TODO -^^^^ +ExifTags.IFD.Makernote +^^^^^^^^^^^^^^^^^^^^^^ -TODO +``ExifTags.IFD.Makernote`` has been deprecated. Instead, use +``ExifTags.IFD.MakerNote``. API Changes =========== diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 207d4de4e24..2280d5ce84b 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -353,6 +353,7 @@ class IFD(IntEnum): Exif = 0x8769 GPSInfo = 0x8825 MakerNote = 0x927C + Makernote = 0x927C # Deprecated Interop = 0xA005 IFD1 = -1 From 2ac383028a1983bb2bee27cd8998c25c81e93e49 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 01:26:13 +1100 Subject: [PATCH 121/175] Allow saving as BigTIFF --- Tests/test_file_tiff.py | 7 +++++ docs/handbook/image-file-formats.rst | 3 ++ src/PIL/TiffImagePlugin.py | 44 +++++++++++++++++----------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 6f51d46513e..df2c4ebeaae 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -115,6 +115,13 @@ def test_bigtiff(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) + def test_bigtiff_save(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + hopper().save(outfile, bigtiff=True) + + with Image.open(outfile) as im: + assert im.tag_v2._bigtiff is True + def test_seek_too_large(self) -> None: with pytest.raises(ValueError, match="Unable to seek to frame"): Image.open("Tests/images/seek_too_large.tif") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 2ea49282ecf..d956d12d1e1 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1208,6 +1208,9 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 8.4.0 +**bigtiff** + If true, the image will be saved as a BigTIFF. + **compression** A string containing the desired compression method for the file. (valid only with libtiff installed) Valid compression diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab760c8fbd6..013f34a4f1d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __init__( self, - ifh: bytes = b"II\052\0\0\0\0\0", + ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00", prefix: bytes | None = None, group: int | None = None, ) -> None: @@ -949,16 +949,26 @@ def load(self, fp: IO[bytes]) -> None: warnings.warn(str(msg)) return + def _get_ifh(self): + ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) + if self._bigtiff: + ifh += self._pack("HH", 8, 0) + ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8) + + return ifh + def tobytes(self, offset: int = 0) -> bytes: # FIXME What about tagdata? - result = self._pack("H", len(self._tags_v2)) + result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) entries: list[tuple[int, int, int, bytes, bytes]] = [] - offset = offset + len(result) + len(self._tags_v2) * 12 + 4 + offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4 stripoffsets = None # pass 1: convert tags to binary format # always write tags in ascending order + fmt = "Q" if self._bigtiff else "L" + fmt_size = 8 if self._bigtiff else 4 for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) @@ -966,11 +976,7 @@ def tobytes(self, offset: int = 0) -> bytes: logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) is_ifd = typ == TiffTags.LONG and isinstance(value, dict) if is_ifd: - if self._endian == "<": - ifh = b"II\x2A\x00\x08\x00\x00\x00" - else: - ifh = b"MM\x00\x2A\x00\x00\x00\x08" - ifd = ImageFileDirectory_v2(ifh, group=tag) + ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag) values = self._tags_v2[tag] for ifd_tag, ifd_value in values.items(): ifd[ifd_tag] = ifd_value @@ -993,10 +999,10 @@ def tobytes(self, offset: int = 0) -> bytes: else: count = len(values) # figure out if data fits into the entry - if len(data) <= 4: - entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) + if len(data) <= fmt_size: + entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b"")) else: - entries.append((tag, typ, count, self._pack("L", offset), data)) + entries.append((tag, typ, count, self._pack(fmt, offset), data)) offset += (len(data) + 1) // 2 * 2 # pad to word # update strip offset data to point beyond auxiliary data @@ -1007,13 +1013,15 @@ def tobytes(self, offset: int = 0) -> bytes: values = [val + offset for val in handler(self, data, self.legacy_api)] data = self._write_dispatch[typ](self, *values) else: - value = self._pack("L", self._unpack("L", value)[0] + offset) + value = self._pack(fmt, self._unpack(fmt, value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data # pass 2: write entries to file for tag, typ, count, value, data in entries: logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) - result += self._pack("HHL4s", tag, typ, count, value) + result += self._pack( + "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value + ) # -- overwrite here for multi-page -- result += b"\0\0\0\0" # end of entries @@ -1028,8 +1036,7 @@ def tobytes(self, offset: int = 0) -> bytes: def save(self, fp: IO[bytes]) -> int: if fp.tell() == 0: # skip TIFF header on subsequent pages - # tiff header -- PIL always starts the first IFD at offset 8 - fp.write(self._prefix + self._pack("HL", 42, 8)) + fp.write(self._get_ifh()) offset = fp.tell() result = self.tobytes(offset) @@ -1680,10 +1687,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: msg = f"cannot write mode {im.mode} as TIFF" raise OSError(msg) from e - ifd = ImageFileDirectory_v2(prefix=prefix) - encoderinfo = im.encoderinfo encoderconfig = im.encoderconfig + + ifd = ImageFileDirectory_v2(prefix=prefix) + if encoderinfo.get("bigtiff"): + ifd._bigtiff = True + try: compression = encoderinfo["compression"] except KeyError: From 8bdcadcbe999c9a2becd6aa2997eb4d74f8ddf2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 10:16:00 +1100 Subject: [PATCH 122/175] Renamed argument to big_tiff --- Tests/test_file_tiff.py | 2 +- docs/handbook/image-file-formats.rst | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index df2c4ebeaae..dedd48c20b3 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -117,7 +117,7 @@ def test_bigtiff(self, tmp_path: Path) -> None: def test_bigtiff_save(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") - hopper().save(outfile, bigtiff=True) + hopper().save(outfile, big_tiff=True) with Image.open(outfile) as im: assert im.tag_v2._bigtiff is True diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index d956d12d1e1..4a220aae64d 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1208,7 +1208,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 8.4.0 -**bigtiff** +**big_tiff** If true, the image will be saved as a BigTIFF. **compression** diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 013f34a4f1d..61eb1524311 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1691,7 +1691,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: encoderconfig = im.encoderconfig ifd = ImageFileDirectory_v2(prefix=prefix) - if encoderinfo.get("bigtiff"): + if encoderinfo.get("big_tiff"): ifd._bigtiff = True try: From e27115ee8da08ace01308b6cf6f66ccb75bda360 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 23:31:05 +0000 Subject: [PATCH 123/175] Update dependency mypy to v1.14.1 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index cd1b1a1a1dc..10e59b885ef 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.14.0 +mypy==1.14.1 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython From 1de617fbe725dcf0862b0d036e3d9cffe05b089f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 11:13:14 +1100 Subject: [PATCH 124/175] Added release notes --- docs/handbook/image-file-formats.rst | 2 ++ docs/releasenotes/11.1.0.rst | 26 +++++++------------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 4a220aae64d..a915ee4e22e 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1211,6 +1211,8 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum **big_tiff** If true, the image will be saved as a BigTIFF. + .. versionadded:: 11.1.0 + **compression** A string containing the desired compression method for the file. (valid only with libtiff installed) Valid compression diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index 27264d99a9e..1505310fa4e 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -1,25 +1,6 @@ 11.1.0 ------ -Security -======== - -TODO -^^^^ - -TODO - -:cve:`YYYY-XXXXX`: TODO -^^^^^^^^^^^^^^^^^^^^^^^ - -TODO - -Backwards Incompatible Changes -============================== - -TODO -^^^^ - Deprecations ============ @@ -66,6 +47,13 @@ zlib library, and what version of zlib-ng is being used:: features.check_feature("zlib_ng") # True or False features.version_feature("zlib_ng") # "2.2.2" for example, or None +Saving TIFF as BigTIFF +^^^^^^^^^^^^^^^^^^^^^^ + +TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument:: + + im.save("out.tiff", big_tiff=True) + Other Changes ============= From f91b111fac15e7e10be7323b291a15e238ba25b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 20:42:49 +1100 Subject: [PATCH 125/175] Removed pre-C99 definitions --- src/libImaging/ImPlatform.h | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index c9b7e43b425..2ce282241d5 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -44,8 +44,6 @@ defines their own types with the same names, so we need to be able to undef ours before including the JPEG code. */ -#if __STDC_VERSION__ >= 199901L /* C99+ */ - #include #define INT8 int8_t @@ -55,34 +53,6 @@ #define INT32 int32_t #define UINT32 uint32_t -#else /* < C99 */ - -#define INT8 signed char - -#if SIZEOF_SHORT == 2 -#define INT16 short -#elif SIZEOF_INT == 2 -#define INT16 int -#else -#error Cannot find required 16-bit integer type -#endif - -#if SIZEOF_SHORT == 4 -#define INT32 short -#elif SIZEOF_INT == 4 -#define INT32 int -#elif SIZEOF_LONG == 4 -#define INT32 long -#else -#error Cannot find required 32-bit integer type -#endif - -#define UINT8 unsigned char -#define UINT16 unsigned INT16 -#define UINT32 unsigned INT32 - -#endif /* < C99 */ - #endif /* not WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ From d42f22baafca30050f4fc8b6bafcc39ef624d685 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 21:38:05 +1100 Subject: [PATCH 126/175] Added release notes --- docs/releasenotes/11.1.0.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst index 27264d99a9e..aec7633a24b 100644 --- a/docs/releasenotes/11.1.0.rst +++ b/docs/releasenotes/11.1.0.rst @@ -80,6 +80,11 @@ Saving JPEG 2000 CMYK images With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files. +Minimum C version +^^^^^^^^^^^^^^^^^ + +C99 is now the minimum version of C required to compile Pillow from source. + zlib-ng in wheels ^^^^^^^^^^^^^^^^^ From 06e02cc1d98dbcfb09839da080ee8eb318baa4ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 20:48:55 +1100 Subject: [PATCH 127/175] Added compile-time mozjpeg feature flag --- src/PIL/features.py | 4 +++- src/_imaging.c | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index 3645e3defc4..ae7ea4255ef 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -127,6 +127,7 @@ def get_supported_codecs() -> list[str]: "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"), "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), "xcb": ("PIL._imaging", "HAVE_XCB", None), @@ -300,7 +301,8 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: if name == "jpg": libjpeg_turbo_version = version_feature("libjpeg_turbo") if libjpeg_turbo_version is not None: - v = "libjpeg-turbo " + libjpeg_turbo_version + v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo" + v += " " + libjpeg_turbo_version if v is None: v = version(name) if v is not None: diff --git a/src/_imaging.c b/src/_imaging.c index 5d6d97bedab..00772d01275 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -76,6 +76,13 @@ #ifdef HAVE_LIBJPEG #include "jconfig.h" +#ifdef LIBJPEG_TURBO_VERSION +#define JCONFIG_INCLUDED +#ifdef __CYGWIN__ +#define _BASETSD_H +#endif +#include "jpeglib.h" +#endif #endif #ifdef HAVE_LIBZ @@ -4367,6 +4374,15 @@ setup_module(PyObject *m) { Py_INCREF(have_libjpegturbo); PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); + PyObject *have_mozjpeg; +#ifdef JPEG_C_PARAM_SUPPORTED + have_mozjpeg = Py_True; +#else + have_mozjpeg = Py_False; +#endif + Py_INCREF(have_mozjpeg); + PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg); + PyObject *have_libimagequant; #ifdef HAVE_LIBIMAGEQUANT have_libimagequant = Py_True; From ae59b039564eefdebec4ea67a712a115d0e1ab67 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 21:40:12 +1100 Subject: [PATCH 128/175] Do not use MozJPEG progressive default --- Tests/test_file_jpeg.py | 13 ++++++++++--- src/libImaging/JpegEncode.c | 11 ++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index bf0dec4b80e..d4c0636c668 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -281,7 +281,10 @@ def test_progressive(self) -> None: assert not im2.info.get("progressive") assert im3.info.get("progressive") - assert_image_equal(im1, im3) + if features.check_feature("mozjpeg"): + assert_image_similar(im1, im3, 9.39) + else: + assert_image_equal(im1, im3) assert im1_bytes >= im3_bytes def test_progressive_large_buffer(self, tmp_path: Path) -> None: @@ -424,8 +427,12 @@ def test_progressive_compat(self) -> None: im2 = self.roundtrip(hopper(), progressive=1) im3 = self.roundtrip(hopper(), progression=1) # compatibility - assert_image_equal(im1, im2) - assert_image_equal(im1, im3) + if features.check_feature("mozjpeg"): + assert_image_similar(im1, im2, 9.39) + assert_image_similar(im1, im3, 9.39) + else: + assert_image_equal(im1, im2) + assert_image_equal(im1, im3) assert im2.info.get("progressive") assert im2.info.get("progression") assert im3.info.get("progressive") diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 4372d51d5c3..3c11eac2206 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -134,7 +134,16 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { return -1; } - /* Compressor configuration */ + /* Compressor configuration */ +#ifdef JPEG_C_PARAM_SUPPORTED + /* MozJPEG */ + if (!context->progressive) { + /* Do not use MozJPEG progressive default */ + jpeg_c_set_int_param( + &context->cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST + ); + } +#endif jpeg_set_defaults(&context->cinfo); /* Prevent RGB -> YCbCr conversion */ From e34427167ddbaeece43490c4054c1e17fa21d77b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Dec 2024 23:26:09 +1100 Subject: [PATCH 129/175] Added CentOS Stream 10 --- .github/workflows/test-docker.yml | 1 + docs/installation/platform-support.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index cc5f9d4a5a9..4b01a10e4c2 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -44,6 +44,7 @@ jobs: amazon-2023-amd64, arch, centos-stream-9-amd64, + centos-stream-10-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, fedora-40-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 35f863374d1..3741c595602 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -27,6 +27,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| CentOS Stream 10 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 40 | 3.12 | x86-64 | From d626e6ab9f37c6bc27036982e282d42012ed0cab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 09:07:41 +1100 Subject: [PATCH 130/175] text is a property --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 974e1e75faa..d87883279a3 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -618,7 +618,7 @@ def test_textual_chunks_after_idat(self) -> None: with Image.open("Tests/images/truncated_image.png") as im: # The file is truncated with pytest.raises(OSError): - im.text() + im.text ImageFile.LOAD_TRUNCATED_IMAGES = True assert isinstance(im.text, dict) ImageFile.LOAD_TRUNCATED_IMAGES = False From 8d78cfcc5a798c59a193b80e200d9845992326ab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 09:10:16 +1100 Subject: [PATCH 131/175] Added return types --- Tests/test_file_jpeg.py | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index bf0dec4b80e..52fc9239c78 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -181,7 +181,7 @@ def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None: assert test(100, 200) == (100, 200) assert test(0) is None # square pixels - def test_dpi_jfif_cm(self): + def test_dpi_jfif_cm(self) -> None: with Image.open("Tests/images/jfif_unit_cm.jpg") as im: assert im.info["dpi"] == (2.54, 5.08) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 61eb1524311..93ad8903244 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -949,7 +949,7 @@ def load(self, fp: IO[bytes]) -> None: warnings.warn(str(msg)) return - def _get_ifh(self): + def _get_ifh(self) -> bytes: ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42) if self._bigtiff: ifh += self._pack("HH", 8, 0) From beda2b6e8d20050a16fbc261753fd30e410fba93 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 10:49:24 +1100 Subject: [PATCH 132/175] Removed unused image open --- Tests/test_file_ico.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 37770498a0a..e81aae66995 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -253,8 +253,7 @@ def test_truncated_mask() -> None: try: with Image.open(io.BytesIO(data)) as im: - with Image.open("Tests/images/hopper_mask.png") as expected: - assert im.mode == "1" + assert im.mode == "1" # 32 bpp output = io.BytesIO() From b89cc09944b4add584967bf1fa21208e92442def Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 12:22:55 +1100 Subject: [PATCH 133/175] Corrected BLP1 alpha depth handling --- Tests/test_file_blp.py | 1 + src/PIL/BlpImagePlugin.py | 43 ++++++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 1e2f20c407b..1f32be9c134 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -19,6 +19,7 @@ def test_load_blp1() -> None: assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im: + assert im.mode == "RGBA" im.load() diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 2d03af9d7dd..0d882fe9686 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -260,18 +260,21 @@ class BlpImageFile(ImageFile.ImageFile): def _open(self) -> None: self.magic = self.fp.read(4) - self.fp.seek(5, os.SEEK_CUR) - (self._blp_alpha_depth,) = struct.unpack(" None: self.fd.seek(4) (self._blp_compression,) = struct.unpack(" None: assert im.palette is not None fp.write(struct.pack(" Date: Wed, 1 Jan 2025 22:58:04 +1100 Subject: [PATCH 134/175] Do not reread start of header in decoder --- src/PIL/BlpImagePlugin.py | 125 +++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 64 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 0d882fe9686..c932b3b9c8a 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -259,24 +259,36 @@ class BlpImageFile(ImageFile.ImageFile): def _open(self) -> None: self.magic = self.fp.read(4) + if not _accept(self.magic): + msg = f"Bad BLP magic {repr(self.magic)}" + raise BLPFormatError(msg) + compression = struct.unpack(" tuple[int, int]: try: - self._read_blp_header() + self._read_header() self._load() except struct.error as e: msg = "Truncated BLP file" @@ -295,28 +307,9 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int def _load(self) -> None: pass - def _read_blp_header(self) -> None: - assert self.fd is not None - self.fd.seek(4) - (self._blp_compression,) = struct.unpack(" None: + self._offsets = struct.unpack("<16I", self._safe_read(16 * 4)) + self._lengths = struct.unpack("<16I", self._safe_read(16 * 4)) def _safe_read(self, length: int) -> bytes: assert self.fd is not None @@ -332,9 +325,11 @@ def _read_palette(self) -> list[tuple[int, int, int, int]]: ret.append((b, g, r, a)) return ret - def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray: + def _read_bgra( + self, palette: list[tuple[int, int, int, int]], alpha: bool + ) -> bytearray: data = bytearray() - _data = BytesIO(self._safe_read(self._blp_lengths[0])) + _data = BytesIO(self._safe_read(self._lengths[0])) while True: try: (offset,) = struct.unpack(" bytearray: break b, g, r, a = palette[offset] d: tuple[int, ...] = (r, g, b) - if self._blp_alpha_depth: + if alpha: d += (a,) data.extend(d) return data @@ -350,19 +345,21 @@ def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray: class BLP1Decoder(_BLPBaseDecoder): def _load(self) -> None: - if self._blp_compression == Format.JPEG: + self._compression, self._encoding, alpha = self.args + + if self._compression == Format.JPEG: self._decode_jpeg_stream() - elif self._blp_compression == 1: - if self._blp_encoding in (4, 5): + elif self._compression == 1: + if self._encoding in (4, 5): palette = self._read_palette() - data = self._read_bgra(palette) + data = self._read_bgra(palette, alpha) self.set_as_raw(data) else: - msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}" + msg = f"Unsupported BLP encoding {repr(self._encoding)}" raise BLPFormatError(msg) else: - msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" + msg = f"Unsupported BLP compression {repr(self._encoding)}" raise BLPFormatError(msg) def _decode_jpeg_stream(self) -> None: @@ -371,8 +368,8 @@ def _decode_jpeg_stream(self) -> None: (jpeg_header_size,) = struct.unpack(" None: class BLP2Decoder(_BLPBaseDecoder): def _load(self) -> None: + self._compression, self._encoding, alpha, self._alpha_encoding = self.args + palette = self._read_palette() assert self.fd is not None - self.fd.seek(self._blp_offsets[0]) + self.fd.seek(self._offsets[0]) - if self._blp_compression == 1: + if self._compression == 1: # Uncompressed or DirectX compression - if self._blp_encoding == Encoding.UNCOMPRESSED: - data = self._read_bgra(palette) + if self._encoding == Encoding.UNCOMPRESSED: + data = self._read_bgra(palette, alpha) - elif self._blp_encoding == Encoding.DXT: + elif self._encoding == Encoding.DXT: data = bytearray() - if self._blp_alpha_encoding == AlphaEncoding.DXT1: - linesize = (self.size[0] + 3) // 4 * 8 - for yb in range((self.size[1] + 3) // 4): - for d in decode_dxt1( - self._safe_read(linesize), alpha=bool(self._blp_alpha_depth) - ): + if self._alpha_encoding == AlphaEncoding.DXT1: + linesize = (self.state.xsize + 3) // 4 * 8 + for yb in range((self.state.ysize + 3) // 4): + for d in decode_dxt1(self._safe_read(linesize), alpha): data += d - elif self._blp_alpha_encoding == AlphaEncoding.DXT3: - linesize = (self.size[0] + 3) // 4 * 16 - for yb in range((self.size[1] + 3) // 4): + elif self._alpha_encoding == AlphaEncoding.DXT3: + linesize = (self.state.xsize + 3) // 4 * 16 + for yb in range((self.state.ysize + 3) // 4): for d in decode_dxt3(self._safe_read(linesize)): data += d - elif self._blp_alpha_encoding == AlphaEncoding.DXT5: - linesize = (self.size[0] + 3) // 4 * 16 - for yb in range((self.size[1] + 3) // 4): + elif self._alpha_encoding == AlphaEncoding.DXT5: + linesize = (self.state.xsize + 3) // 4 * 16 + for yb in range((self.state.ysize + 3) // 4): for d in decode_dxt5(self._safe_read(linesize)): data += d else: - msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" + msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}" raise BLPFormatError(msg) else: - msg = f"Unknown BLP encoding {repr(self._blp_encoding)}" + msg = f"Unknown BLP encoding {repr(self._encoding)}" raise BLPFormatError(msg) else: - msg = f"Unknown BLP compression {repr(self._blp_compression)}" + msg = f"Unknown BLP compression {repr(self._compression)}" raise BLPFormatError(msg) self.set_as_raw(data) From 5d998d3fedb06666ae680e3ebe3f3547a9059727 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 23:38:24 +1100 Subject: [PATCH 135/175] Improved coverage --- Tests/test_file_blp.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 1f32be9c134..9f2de8f982e 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -4,7 +4,7 @@ import pytest -from PIL import Image +from PIL import BlpImagePlugin, Image from .helper import ( assert_image_equal, @@ -38,6 +38,13 @@ def test_load_blp2_dxt1a() -> None: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") +def test_invalid_file() -> None: + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(BlpImagePlugin.BLPFormatError): + BlpImagePlugin.BlpImageFile(invalid_file) + + def test_save(tmp_path: Path) -> None: f = str(tmp_path / "temp.blp") From f636cb8c156f53cb3acd3ebf7164113850df3f27 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 10:28:51 +1100 Subject: [PATCH 136/175] Updated freetype to 2.13.3 --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4e0fad79f4e..9059c04a4f5 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -37,7 +37,7 @@ fi ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds -FREETYPE_VERSION=2.13.2 +FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.44 JPEGTURBO_VERSION=3.1.0 From 4c1aed801e43c6b307e7135279ca1dbc02bbf052 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 16:00:59 +1100 Subject: [PATCH 137/175] 11.1.0 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 0807f949c31..9938a0afcf6 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.1.0.dev0" +__version__ = "11.1.0" From 57786a252b2e3abd63242800ab06511bb315b2d8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 19:04:18 +1100 Subject: [PATCH 138/175] 11.2.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 9938a0afcf6..e93c7887b80 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "11.1.0" +__version__ = "11.2.0.dev0" From 6b4619c4f5998d8d40de32de7b17b664d9b8a0db Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 20:46:58 +1100 Subject: [PATCH 139/175] Updated macOS tested Pillow versions --- docs/installation/platform-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 3741c595602..7561946792f 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -77,7 +77,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+============================+==================+==============+ -| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm | +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm | | +----------------------------+------------------+ | | | 3.8 | 10.4.0 | | +----------------------------------+----------------------------+------------------+--------------+ From ade15fcdd3c9f41606ce560c4b5fdeb01f0025e2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:46:24 +0200 Subject: [PATCH 140/175] Upgrade zlib-ng to 2.2.3 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4e0fad79f4e..e89db5020f8 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -50,7 +50,7 @@ if [[ -n "$IS_MACOS" ]]; then else GIFLIB_VERSION=5.2.1 fi -ZLIB_NG_VERSION=2.2.2 +ZLIB_NG_VERSION=2.2.3 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0674a9a1528..75d6aa1bd2d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ def cmd_msbuild( "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.3", - "ZLIBNG": "2.2.2", + "ZLIBNG": "2.2.3", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) From 2d7597ac6a431d283a65d1d17622a6d8f9918010 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jan 2025 22:50:25 +1100 Subject: [PATCH 141/175] Updated to giflib 5.2.2 on Linux --- .github/workflows/wheels-dependencies.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4e0fad79f4e..71609a6f49a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -45,11 +45,7 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -if [[ -n "$IS_MACOS" ]]; then - GIFLIB_VERSION=5.2.2 -else - GIFLIB_VERSION=5.2.1 -fi +GIFLIB_VERSION=5.2.2 ZLIB_NG_VERSION=2.2.2 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 @@ -139,6 +135,14 @@ function build { CFLAGS="$CFLAGS -O3 -DNDEBUG" if [[ -n "$IS_MACOS" ]]; then CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + # For giflib 5.2.2 + elif [ -n "$IS_ALPINE" ]; then + apk add imagemagick + else + if [[ "$MB_ML_VER" == "_2_28" ]]; then + yum install -y epel-release + fi + yum install -y ImageMagick fi build_libwebp CFLAGS=$ORIGINAL_CFLAGS From 1678f7f2155beafa594c3561179f4069f9318d35 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:38:21 +0100 Subject: [PATCH 142/175] Add overloads for exif_transpose --- src/PIL/ImageOps.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index bb29cc0d3e8..fef1d7328c2 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -22,7 +22,7 @@ import operator import re from collections.abc import Sequence -from typing import Protocol, cast +from typing import Literal, Protocol, cast, overload from . import ExifTags, Image, ImagePalette @@ -673,6 +673,16 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: return _lut(image, lut) +@overload +def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ... + + +@overload +def exif_transpose( + image: Image.Image, *, in_place: Literal[False] = False +) -> Image.Image: ... + + def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: """ If an image has an EXIF Orientation tag, other than 1, transpose the image From 1d771ff4a40e8eb9a38c150d18767cddf01c8a47 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Jan 2025 10:26:47 +1100 Subject: [PATCH 143/175] Do not call yum on cifuzz --- .github/workflows/wheels-dependencies.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1ebc49a88e0..ceb7911be5a 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -99,7 +99,7 @@ function build_harfbuzz { function build { build_xz - if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then + if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi build_zlib_ng @@ -138,6 +138,8 @@ function build { # For giflib 5.2.2 elif [ -n "$IS_ALPINE" ]; then apk add imagemagick + elif [ -n "$SANITIZER" ]; then + apt-get install -y imagemagick else if [[ "$MB_ML_VER" == "_2_28" ]]; then yum install -y epel-release From d12e78badf1fc4a102b4bec044eb12a6bfd5d0aa Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 3 Jan 2025 11:00:19 +1100 Subject: [PATCH 144/175] Removed exif_transpose return type checks --- Tests/test_file_jpeg.py | 1 - Tests/test_imageops.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index bf0dec4b80e..dd62460bb5d 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -353,7 +353,6 @@ def test_empty_exif_gps(self) -> None: assert exif.get_ifd(0x8825) == {} transposed = ImageOps.exif_transpose(im) - assert transposed is not None exif = transposed.getexif() assert exif.get_ifd(0x8825) == {} diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 2fb2a60b632..7262f29e64a 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -405,7 +405,6 @@ def check(orientation_im: Image.Image) -> None: else: original_exif = im.info["exif"] transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert_image_similar(base_im, transposed_im, 17) if orientation_im is base_im: assert "exif" not in im.info @@ -417,7 +416,6 @@ def check(orientation_im: Image.Image) -> None: # Repeat the operation to test that it does not keep transposing transposed_im2 = ImageOps.exif_transpose(transposed_im) - assert transposed_im2 is not None assert_image_equal(transposed_im2, transposed_im) check(base_im) @@ -433,7 +431,6 @@ def check(orientation_im: Image.Image) -> None: assert im.getexif()[0x0112] == 3 transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() transposed_im._reload_exif() @@ -446,14 +443,12 @@ def check(orientation_im: Image.Image) -> None: assert im.getexif()[0x0112] == 3 transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() # Orientation set directly on Image.Exif im = hopper() im.getexif()[0x0112] = 3 transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() @@ -464,7 +459,6 @@ def test_exif_transpose_xml_without_xmp() -> None: del im.info["xmp"] transposed_im = ImageOps.exif_transpose(im) - assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() From 036db2da87dba7283207b8b61cf9ca131d1223a3 Mon Sep 17 00:00:00 2001 From: "Harm.van.den.brand@alliander.com" Date: Thu, 2 Jan 2025 16:47:24 +0100 Subject: [PATCH 145/175] OSError caused by decode error should use string argument to be in line with rest of module --- Tests/test_file_libtiff.py | 2 +- src/PIL/TiffImagePlugin.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9c49b1534ed..49d71aca77f 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1146,7 +1146,7 @@ def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: im.load() # Assert that the error code is IMAGING_CODEC_MEMORY - assert str(e.value) == "-9" + assert str(e.value) == "decoder error -9" @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 61eb1524311..bbbd656c68a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1406,7 +1406,8 @@ def _load_libtiff(self) -> Image.core.PixelAccess | None: self.fp = None # might be shared if err < 0: - raise OSError(err) + msg = f"decoder error {err}" + raise OSError(msg) return Image.Image.load(self) From cce0f5b653abcdf99a7bbba6757f000b3fc4cd7e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 4 Jan 2025 10:34:59 +1100 Subject: [PATCH 146/175] Removed giflib as webp dependency --- .github/workflows/wheels-dependencies.sh | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 1ebc49a88e0..05167d96962 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -45,7 +45,6 @@ OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 -GIFLIB_VERSION=5.2.2 ZLIB_NG_VERSION=2.2.3 LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 @@ -135,16 +134,10 @@ function build { CFLAGS="$CFLAGS -O3 -DNDEBUG" if [[ -n "$IS_MACOS" ]]; then CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" - # For giflib 5.2.2 - elif [ -n "$IS_ALPINE" ]; then - apk add imagemagick - else - if [[ "$MB_ML_VER" == "_2_28" ]]; then - yum install -y epel-release - fi - yum install -y ImageMagick fi - build_libwebp + build_simple libwebp $LIBWEBP_VERSION \ + https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ + --enable-libwebpmux --enable-libwebpdemux CFLAGS=$ORIGINAL_CFLAGS build_brotli From bd56a956594445c9b2e0bd5004f1b5c1a3f96b38 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Jan 2025 12:43:50 +1100 Subject: [PATCH 147/175] Use namedtuple _replace --- src/PIL/BlpImagePlugin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index c932b3b9c8a..b8a95db879d 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -374,11 +374,9 @@ def _decode_jpeg_stream(self) -> None: image = JpegImageFile(BytesIO(data)) Image._decompression_bomb_check(image.size) if image.mode == "CMYK": - decoder_name, extents, offset, args = image.tile[0] + args = image.tile[0].args assert isinstance(args, tuple) - image.tile = [ - ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK")) - ] + image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))] r, g, b = image.convert("RGB").split() reversed_image = Image.merge("RGB", (b, g, r)) self.set_as_raw(reversed_image.tobytes()) From 73a383fa7211adf5ed8ffa43288e6bc47daa125e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 5 Jan 2025 06:11:54 +1100 Subject: [PATCH 148/175] Use rawmode instead of splitting and merging --- src/PIL/BlpImagePlugin.py | 4 +--- src/libImaging/Unpack.c | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index b8a95db879d..8585a8e60fd 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -377,9 +377,7 @@ def _decode_jpeg_stream(self) -> None: args = image.tile[0].args assert isinstance(args, tuple) image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))] - r, g, b = image.convert("RGB").split() - reversed_image = Image.merge("RGB", (b, g, r)) - self.set_as_raw(reversed_image.tobytes()) + self.set_as_raw(image.convert("RGB").tobytes(), "BGR") class BLP2Decoder(_BLPBaseDecoder): diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index e9203fe4d74..9c3ee26655f 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1664,6 +1664,7 @@ static struct { {"RGBA", "RGBaXX", 48, unpackRGBaskip2}, {"RGBA", "RGBa;16L", 64, unpackRGBa16L}, {"RGBA", "RGBa;16B", 64, unpackRGBa16B}, + {"RGBA", "BGR", 24, ImagingUnpackBGR}, {"RGBA", "BGRa", 32, unpackBGRa}, {"RGBA", "RGBA;I", 32, unpackRGBAI}, {"RGBA", "RGBA;L", 32, unpackRGBAL}, From 4ecf8cbd75051c7213a433f80b6a9f24e4367311 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 5 Jan 2025 14:49:34 +1100 Subject: [PATCH 149/175] Simplified code --- src/_imagingft.c | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index d38279f3e4b..3a65007a514 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -339,29 +339,23 @@ text_layout_raqm( len = PySequence_Fast_GET_SIZE(seq); for (j = 0; j < len; j++) { PyObject *item = PySequence_Fast_GET_ITEM(seq, j); - char *feature = NULL; - Py_ssize_t size = 0; - PyObject *bytes; - if (!PyUnicode_Check(item)) { Py_DECREF(seq); PyErr_SetString(PyExc_TypeError, "expected a string"); goto failed; } - bytes = PyUnicode_AsUTF8String(item); - if (bytes == NULL) { + + Py_ssize_t size; + const char *feature = PyUnicode_AsUTF8AndSize(item, &size); + if (feature == NULL) { Py_DECREF(seq); goto failed; } - feature = PyBytes_AS_STRING(bytes); - size = PyBytes_GET_SIZE(bytes); if (!raqm_add_font_feature(rq, feature, size)) { Py_DECREF(seq); - Py_DECREF(bytes); PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); goto failed; } - Py_DECREF(bytes); } Py_DECREF(seq); } From 7708e4b524aca2e7d56fcc75eee59834622f75e8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 6 Jan 2025 20:30:47 +1100 Subject: [PATCH 150/175] Improved Docker coverage reporting --- .ci/after_success.sh | 6 +----- .github/workflows/test-docker.yml | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.ci/after_success.sh b/.ci/after_success.sh index c71546f007b..6da27b975cc 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -2,8 +2,4 @@ # gather the coverage data python3 -m pip install coverage -if [[ $MATRIX_DOCKER ]]; then - python3 -m coverage xml --ignore-errors -else - python3 -m coverage xml -fi +python3 -m coverage xml diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 4b01a10e4c2..bebb9cda277 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -90,15 +90,15 @@ jobs: - name: After success run: | - PATH="$PATH:~/.local/bin" docker start pillow_container + sudo docker cp pillow_container:/Pillow /Pillow + sudo chown -R runner /Pillow pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'` docker stop pillow_container sudo mkdir -p $pil_path sudo cp src/PIL/*.py $pil_path + cd /Pillow .ci/after_success.sh - env: - MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage uses: codecov/codecov-action@v5 From b1749dff08ab96a05234e1492759011ef54cbd59 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:35:41 +0000 Subject: [PATCH 151/175] [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.8.4 → v0.8.6](/~https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.8.6) - [github.com/pre-commit/mirrors-clang-format: v19.1.5 → v19.1.6](/~https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.5...v19.1.6) - [github.com/woodruffw/zizmor-pre-commit: v0.10.0 → v1.0.0](/~https://github.com/woodruffw/zizmor-pre-commit/compare/v0.10.0...v1.0.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b76f92ec00e..20fa7d04f00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: /~https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.8.6 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -24,7 +24,7 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: /~https://github.com/pre-commit/mirrors-clang-format - rev: v19.1.5 + rev: v19.1.6 hooks: - id: clang-format types: [c] @@ -57,7 +57,7 @@ repos: - id: check-renovate - repo: /~https://github.com/woodruffw/zizmor-pre-commit - rev: v0.10.0 + rev: v1.0.0 hooks: - id: zizmor From 618339e2d2e9313ab8f2da0d78efec25477c4b43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 6 Jan 2025 07:28:38 +1100 Subject: [PATCH 152/175] Allow saving multiple frames as BigTIFF --- Tests/test_file_tiff.py | 19 +++++++++-- src/PIL/TiffImagePlugin.py | 69 +++++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index dedd48c20b3..c4a33488136 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -117,10 +117,16 @@ def test_bigtiff(self, tmp_path: Path) -> None: def test_bigtiff_save(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") - hopper().save(outfile, big_tiff=True) + im = hopper() + im.save(outfile, big_tiff=True) - with Image.open(outfile) as im: - assert im.tag_v2._bigtiff is True + with Image.open(outfile) as reloaded: + assert reloaded.tag_v2._bigtiff is True + + im.save(outfile, save_all=True, append_images=[im], big_tiff=True) + + with Image.open(outfile) as reloaded: + assert reloaded.tag_v2._bigtiff is True def test_seek_too_large(self) -> None: with pytest.raises(ValueError, match="Unable to seek to frame"): @@ -753,6 +759,13 @@ def test_fixoffsets(self) -> None: with pytest.raises(RuntimeError): a.fixOffsets(1) + def test_appending_tiff_writer_writelong(self) -> None: + data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b = BytesIO(data) + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.writeLong(2**32 - 1) + assert b.getvalue() == data + b"\xff\xff\xff\xff" + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 61eb1524311..5dd56d92b5a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -962,13 +962,16 @@ def tobytes(self, offset: int = 0) -> bytes: result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2)) entries: list[tuple[int, int, int, bytes, bytes]] = [] - offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4 + + fmt = "Q" if self._bigtiff else "L" + fmt_size = 8 if self._bigtiff else 4 + offset += ( + len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size + ) stripoffsets = None # pass 1: convert tags to binary format # always write tags in ascending order - fmt = "Q" if self._bigtiff else "L" - fmt_size = 8 if self._bigtiff else 4 for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) @@ -1024,7 +1027,7 @@ def tobytes(self, offset: int = 0) -> bytes: ) # -- overwrite here for multi-page -- - result += b"\0\0\0\0" # end of entries + result += self._pack(fmt, 0) # end of entries # pass 3: write auxiliary data to file for tag, typ, count, value, data in entries: @@ -2043,20 +2046,21 @@ def setup(self) -> None: self.offsetOfNewPage = 0 self.IIMM = iimm = self.f.read(4) + self._bigtiff = b"\x2B" in iimm if not iimm: # empty file - first page self.isFirst = True return self.isFirst = False - if iimm == b"II\x2a\x00": - self.setEndian("<") - elif iimm == b"MM\x00\x2a": - self.setEndian(">") - else: + if iimm not in PREFIXES: msg = "Invalid TIFF file header" raise RuntimeError(msg) + self.setEndian("<" if iimm.startswith(II) else ">") + + if self._bigtiff: + self.f.seek(4, os.SEEK_CUR) self.skipIFDs() self.goToEnd() @@ -2076,11 +2080,13 @@ def finalize(self) -> None: msg = "IIMM of new page doesn't match IIMM of first page" raise RuntimeError(msg) - ifd_offset = self.readLong() + if self._bigtiff: + self.f.seek(4, os.SEEK_CUR) + ifd_offset = self._read(8 if self._bigtiff else 4) ifd_offset += self.offsetOfNewPage assert self.whereToWriteNewIFDOffset is not None self.f.seek(self.whereToWriteNewIFDOffset) - self.writeLong(ifd_offset) + self._write(ifd_offset, 8 if self._bigtiff else 4) self.f.seek(ifd_offset) self.fixIFD() @@ -2126,18 +2132,20 @@ def setEndian(self, endian: str) -> None: self.endian = endian self.longFmt = f"{self.endian}L" self.shortFmt = f"{self.endian}H" - self.tagFormat = f"{self.endian}HHL" + self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L") def skipIFDs(self) -> None: while True: - ifd_offset = self.readLong() + ifd_offset = self._read(8 if self._bigtiff else 4) if ifd_offset == 0: - self.whereToWriteNewIFDOffset = self.f.tell() - 4 + self.whereToWriteNewIFDOffset = self.f.tell() - ( + 8 if self._bigtiff else 4 + ) break self.f.seek(ifd_offset) - num_tags = self.readShort() - self.f.seek(num_tags * 12, os.SEEK_CUR) + num_tags = self._read(8 if self._bigtiff else 2) + self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR) def write(self, data: Buffer, /) -> int: return self.f.write(data) @@ -2185,13 +2193,17 @@ def rewriteLastShort(self, value: int) -> None: def rewriteLastLong(self, value: int) -> None: return self._rewriteLast(value, 4) + def _write(self, value: int, field_size: int) -> None: + bytes_written = self.f.write( + struct.pack(self.endian + self._fmt(field_size), value) + ) + self._verify_bytes_written(bytes_written, field_size) + def writeShort(self, value: int) -> None: - bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - self._verify_bytes_written(bytes_written, 2) + self._write(value, 2) def writeLong(self, value: int) -> None: - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - self._verify_bytes_written(bytes_written, 4) + self._write(value, 4) def close(self) -> None: self.finalize() @@ -2199,24 +2211,27 @@ def close(self) -> None: self.f.close() def fixIFD(self) -> None: - num_tags = self.readShort() + num_tags = self._read(8 if self._bigtiff else 2) for i in range(num_tags): - tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) + tag, field_type, count = struct.unpack( + self.tagFormat, self.f.read(12 if self._bigtiff else 8) + ) field_size = self.fieldSizes[field_type] total_size = field_size * count - is_local = total_size <= 4 + fmt_size = 8 if self._bigtiff else 4 + is_local = total_size <= fmt_size if not is_local: - offset = self.readLong() + self.offsetOfNewPage - self.rewriteLastLong(offset) + offset = self._read(fmt_size) + self.offsetOfNewPage + self._rewriteLast(offset, fmt_size) if tag in self.Tags: cur_pos = self.f.tell() if is_local: self._fixOffsets(count, field_size) - self.f.seek(cur_pos + 4) + self.f.seek(cur_pos + fmt_size) else: self.f.seek(offset) self._fixOffsets(count, field_size) @@ -2224,7 +2239,7 @@ def fixIFD(self) -> None: elif is_local: # skip the locally stored value that is not an offset - self.f.seek(4, os.SEEK_CUR) + self.f.seek(fmt_size, os.SEEK_CUR) def _fixOffsets(self, count: int, field_size: int) -> None: for i in range(count): From a8381c619de0c244785377322ed8bf115a899146 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 6 Jan 2025 07:28:51 +1100 Subject: [PATCH 153/175] Allow upgrading LONG to LONG8 --- Tests/test_file_tiff.py | 26 ++++++++++++++++++++++++- src/PIL/TiffImagePlugin.py | 39 ++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c4a33488136..757d3f96a5f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -746,7 +746,7 @@ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: assert reread.n_frames == 3 def test_fixoffsets(self) -> None: - b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") + b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00") with TiffImagePlugin.AppendingTiffWriter(b) as a: b.seek(0) a.fixOffsets(1, isShort=True) @@ -759,6 +759,23 @@ def test_fixoffsets(self) -> None: with pytest.raises(RuntimeError): a.fixOffsets(1) + b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00") + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.offsetOfNewPage = 2**16 + + b.seek(0) + a.fixOffsets(1, isShort=True) + + b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.offsetOfNewPage = 2**32 + + b.seek(0) + a.fixOffsets(1, isShort=True) + + b.seek(0) + a.fixOffsets(1, isLong=True) + def test_appending_tiff_writer_writelong(self) -> None: data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b = BytesIO(data) @@ -766,6 +783,13 @@ def test_appending_tiff_writer_writelong(self) -> None: a.writeLong(2**32 - 1) assert b.getvalue() == data + b"\xff\xff\xff\xff" + def test_appending_tiff_writer_rewritelastshorttolong(self) -> None: + data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b = BytesIO(data) + with TiffImagePlugin.AppendingTiffWriter(b) as a: + a.rewriteLastShortToLong(2**32 - 1) + assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff" + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 5dd56d92b5a..8179b7f5bd9 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2175,17 +2175,19 @@ def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: msg = f"wrote only {bytes_written} bytes but wanted {expected}" raise RuntimeError(msg) - def rewriteLastShortToLong(self, value: int) -> None: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - self._verify_bytes_written(bytes_written, 4) - - def _rewriteLast(self, value: int, field_size: int) -> None: + def _rewriteLast( + self, value: int, field_size: int, new_field_size: int = 0 + ) -> None: self.f.seek(-field_size, os.SEEK_CUR) + if not new_field_size: + new_field_size = field_size bytes_written = self.f.write( - struct.pack(self.endian + self._fmt(field_size), value) + struct.pack(self.endian + self._fmt(new_field_size), value) ) - self._verify_bytes_written(bytes_written, field_size) + self._verify_bytes_written(bytes_written, new_field_size) + + def rewriteLastShortToLong(self, value: int) -> None: + self._rewriteLast(value, 2, 4) def rewriteLastShort(self, value: int) -> None: return self._rewriteLast(value, 2) @@ -2245,18 +2247,27 @@ def _fixOffsets(self, count: int, field_size: int) -> None: for i in range(count): offset = self._read(field_size) offset += self.offsetOfNewPage - if field_size == 2 and offset >= 65536: - # offset is now too large - we must convert shorts to longs + + new_field_size = 0 + if self._bigtiff and field_size in (2, 4) and offset >= 2**32: + # offset is now too large - we must convert long to long8 + new_field_size = 8 + elif field_size == 2 and offset >= 2**16: + # offset is now too large - we must convert short to long + new_field_size = 4 + if new_field_size: if count != 1: msg = "not implemented" raise RuntimeError(msg) # XXX TODO # simple case - the offset is just one and therefore it is # local (not referenced with another offset) - self.rewriteLastShortToLong(offset) - self.f.seek(-10, os.SEEK_CUR) - self.writeShort(TiffTags.LONG) # rewrite the type to LONG - self.f.seek(8, os.SEEK_CUR) + self._rewriteLast(offset, field_size, new_field_size) + # Move back past the new offset, past 'count', and before 'field_type' + rewind = -new_field_size - 4 - 2 + self.f.seek(rewind, os.SEEK_CUR) + self.writeShort(new_field_size) # rewrite the type + self.f.seek(2 - rewind, os.SEEK_CUR) else: self._rewriteLast(offset, field_size) From aef6df2d04bfe86b4a69ab7d93786ea55a3e7340 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 Jan 2025 21:33:57 +1100 Subject: [PATCH 154/175] Use ImageFile._Tile --- Tests/test_file_jpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index dd62460bb5d..526c6a5b614 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1030,7 +1030,7 @@ def decode( with Image.open(TEST_FILE) as im: im.tile = [ - ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), + ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), ] ImageFile.LOAD_TRUNCATED_IMAGES = True im.load() From f36c66746705245dec44b225868bce727dca0385 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 Jan 2025 22:24:08 +1100 Subject: [PATCH 155/175] Improved test coverage --- Tests/test_file_spider.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 4cafda86536..713db848df8 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -7,7 +7,7 @@ import pytest -from PIL import Image, ImageSequence, SpiderImagePlugin +from PIL import Image, SpiderImagePlugin from .helper import assert_image_equal, hopper, is_pypy @@ -153,8 +153,8 @@ def test_nonstack_file() -> None: def test_nonstack_dos() -> None: with Image.open(TEST_FILE) as im: - for i, frame in enumerate(ImageSequence.Iterator(im)): - assert i <= 1, "Non-stack DOS file test failed" + with pytest.raises(EOFError): + im.seek(0) # for issue #4093 From 86b8e1e45fa1d425aafa444024003eb3b75d8a9d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Jan 2025 10:19:09 +1100 Subject: [PATCH 156/175] Updated libpng to 1.6.45 --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 58621bca1ed..410255b7eda 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -39,7 +39,7 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=10.1.0 -LIBPNG_VERSION=1.6.44 +LIBPNG_VERSION=1.6.45 JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 75d6aa1bd2d..912579ce790 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -116,7 +116,7 @@ def cmd_msbuild( "HARFBUZZ": "10.1.0", "JPEGTURBO": "3.1.0", "LCMS2": "2.16", - "LIBPNG": "1.6.44", + "LIBPNG": "1.6.45", "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", From ee2b8c525632f76bde730805d11d19bbb22f1b2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Jan 2025 10:26:21 +1100 Subject: [PATCH 157/175] Switch to .tar.gz for libpng --- winbuild/build_prepare.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 912579ce790..b9695d1d802 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -123,7 +123,6 @@ def cmd_msbuild( "XZ": "5.6.3", "ZLIBNG": "2.2.3", } -V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) @@ -241,8 +240,8 @@ def cmd_msbuild( }, "libpng": { "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" - f"lpng{V['LIBPNG_DOTLESS']}.zip/download", - "filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", + f"FILENAME/download", + "filename": f"libpng-{V['LIBPNG']}.tar.gz", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), From f281eb9b469320f29006d7454d9c38a974ad65c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Jan 2025 18:27:20 +1100 Subject: [PATCH 158/175] Trigger from changes in pyproject.toml --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3b22ee98a2c..fd89f7585ee 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -13,6 +13,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "pyproject.toml" - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" @@ -23,6 +24,7 @@ on: paths: - ".ci/requirements-cibw.txt" - ".github/workflows/wheel*" + - "pyproject.toml" - "setup.py" - "wheels/*" - "winbuild/build_prepare.py" From 84c8e38b2d88d0c4fb733d9c6e44e6af13e2e05e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 07:38:51 +0000 Subject: [PATCH 159/175] Update cygwin/cygwin-install-action action to v5 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 5b0a0394688..abfeaa77f9c 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -52,7 +52,7 @@ jobs: persist-credentials: false - name: Install Cygwin - uses: cygwin/cygwin-install-action@v4 + uses: cygwin/cygwin-install-action@v5 with: packages: > gcc-g++ From 2eb112329e4df9ca4ea15235c2743da8f6b0eab8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 08:32:42 +1100 Subject: [PATCH 160/175] Use python-pip instead of python3-pip --- .github/workflows/test-mingw.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a1d6ba61c9b..6b1b36cb0ca 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -68,7 +68,7 @@ jobs: mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-olefile \ - mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python-pip \ mingw-w64-x86_64-python-pytest \ mingw-w64-x86_64-python-pytest-cov \ mingw-w64-x86_64-python-pytest-timeout \ From 440b09e831873624f9e843adf658633b25983e77 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:32:17 +1100 Subject: [PATCH 161/175] Removed unused mode argument from assert_image_similar_tofile --- Tests/helper.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index d6a93a8030b..3f45498bb0b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -140,11 +140,8 @@ def assert_image_similar_tofile( 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) From aa686894a63cb2f15349e9592eedb95aebfc6f1f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:32:46 +1100 Subject: [PATCH 162/175] Removed unused assert_all_same --- Tests/helper.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 3f45498bb0b..126644c159b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -145,10 +145,6 @@ def assert_image_similar_tofile( assert_image_similar(a, img, epsilon, msg) -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: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) != len(items), msg From f938af5c3cc9dd7c48f8a06a7af4870f1faf2e76 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:38:07 +1100 Subject: [PATCH 163/175] Do not catch exception only to assert it is None --- Tests/test_file_apng.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index ee6c867c3ca..9d5154fca07 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -307,13 +307,8 @@ def test_apng_syntax_errors() -> None: im.load() # we can handle this case gracefully - exception = None with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: - try: - im.seek(im.n_frames - 1) - except Exception as e: - exception = e - assert exception is None + im.seek(im.n_frames - 1) with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: @@ -405,13 +400,8 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: append_images=frames, ) with Image.open(test_file) as im: - exception = None - try: - im.seek(im.n_frames - 1) - im.load() - except Exception as e: - exception = e - assert exception is None + im.seek(im.n_frames - 1) + im.load() def test_apng_save_duration_loop(tmp_path: Path) -> None: From a34a9cd6d1d4b346573fca7336f1cb82918af4b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:49:48 +1100 Subject: [PATCH 164/175] Improved test coverage --- Tests/test_file_iptc.py | 5 +---- Tests/test_file_jpeg2k.py | 3 +-- Tests/test_file_libtiff.py | 6 +----- Tests/test_image.py | 2 -- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 8a7c59fb150..c6c0c1aab9d 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -58,10 +58,7 @@ def test_getiptcinfo_fotostation() -> None: # Assert assert iptc is not None - for tag in iptc.keys(): - if tag[0] == 240: - return - pytest.fail("FotoStation tag not found") + assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found" def test_getiptcinfo_zero_padding() -> None: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index dbc2e49ec5a..711e988df66 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -492,8 +492,7 @@ def test_plt_marker(card: ImageFile.ImageFile) -> None: out.seek(0) while True: marker = out.read(2) - if not marker: - pytest.fail("End of stream without PLT") + assert marker, "End of stream without PLT" jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 49d71aca77f..18dd11182c3 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -36,11 +36,7 @@ def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> No im.load() im.getdata() - try: - assert im._compression == "group4" - except AttributeError: - print("No _compression") - print(dir(im)) + assert im._compression == "group4" # can we write it back out, in a different form. out = str(tmp_path / "temp.png") diff --git a/Tests/test_image.py b/Tests/test_image.py index 092bc07f6f1..fe43cea407f 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -189,8 +189,6 @@ def test_pathlib(self, tmp_path: Path) -> None: if ext == ".jp2" and not features.check_codec("jpg_2000"): pytest.skip("jpg_2000 not available") temp_file = str(tmp_path / ("temp." + ext)) - if os.path.exists(temp_file): - os.remove(temp_file) im.save(Path(temp_file)) def test_fp_name(self, tmp_path: Path) -> None: From 4d14991604d34f73aa5d426340adf773c81cdc6a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jan 2025 14:58:58 +1100 Subject: [PATCH 165/175] Corrected argument types --- Tests/test_image.py | 4 ++-- Tests/test_image_resize.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 092bc07f6f1..a72694ec1f6 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -667,7 +667,7 @@ def test_remap_palette(self) -> None: # Test illegal image mode with hopper() as im: with pytest.raises(ValueError): - im.remap_palette(None) + im.remap_palette([]) def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) @@ -770,7 +770,7 @@ def test_empty_exif(self) -> None: assert dict(exif) # Test that exif data is cleared after another load - exif.load(None) + exif.load(b"") assert not dict(exif) # Test loading just the EXIF header diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 57fcf9a3463..1166371b8f9 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -309,7 +309,7 @@ def resize(mode: str, size: tuple[int, int] | list[int]) -> None: # Test unknown resampling filter with hopper() as im: with pytest.raises(ValueError): - im.resize((10, 10), "unknown") + im.resize((10, 10), -1) @skip_unless_feature("libtiff") def test_transposed(self) -> None: From 8603d6512a2e6f534d6e0fb4a9307bc3edd4f0fa Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:22:29 +0200 Subject: [PATCH 166/175] Use python-numpy and python-olefile instead of python3-numpy and python3-olefile --- .github/workflows/test-mingw.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6b1b36cb0ca..bb6d7dc373e 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -66,8 +66,8 @@ jobs: mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-python3-numpy \ - mingw-w64-x86_64-python3-olefile \ + mingw-w64-x86_64-python-numpy \ + mingw-w64-x86_64-python-olefile \ mingw-w64-x86_64-python-pip \ mingw-w64-x86_64-python-pytest \ mingw-w64-x86_64-python-pytest-cov \ From 0d93c030a576e30d3991bf5547f2e0c3e47889a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Jan 2025 19:10:42 +1100 Subject: [PATCH 167/175] Test passes in Python 3.13 --- Tests/test_image_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index bb30b462d2f..6d8b4d35579 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -271,7 +271,7 @@ def test_putpixel_overflow_error(self, mode: str) -> None: class TestEmbeddable: - @pytest.mark.xfail(reason="failing test") + @pytest.mark.xfail(not (sys.version_info >= (3, 13)), reason="failing test") @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self) -> None: import ctypes From 64bfdff6c8111225a1c7e4480376738123f2cb15 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Jan 2025 21:51:33 +1100 Subject: [PATCH 168/175] Only F mode starts with F --- src/PIL/SpiderImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 3a87d009adc..b26e1a996b9 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -267,7 +267,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if im.mode[0] != "F": + if im.mode != "F": im = im.convert("F") hdr = makeSpiderHeader(im) From 7166a09538bbadbeb9d02f1d9af0c8d70022a80b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Jan 2025 18:57:41 +1100 Subject: [PATCH 169/175] Skip test_embeddable if compiler cannot be initialized --- Tests/test_image_access.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 6d8b4d35579..14a5e2e7bfe 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -278,6 +278,18 @@ def test_embeddable(self) -> None: from setuptools.command import build_ext + compiler = getattr(build_ext, "new_compiler")() + compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) + + libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( + "INCLUDEPY" + ).replace("include", "libs") + compiler.add_library_dir(libdir) + try: + compiler.initialize() + except Exception: + pytest.skip("Compiler could not be initialized") + with open("embed_pil.c", "w", encoding="utf-8") as fh: home = sys.prefix.replace("\\", "\\\\") fh.write( @@ -305,13 +317,6 @@ def test_embeddable(self) -> None: """ ) - compiler = getattr(build_ext, "new_compiler")() - compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) - - libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( - "INCLUDEPY" - ).replace("include", "libs") - compiler.add_library_dir(libdir) objects = compiler.compile(["embed_pil.c"]) compiler.link_executable(objects, "embed_pil") From a4018d192cf8a305c3da622a53df7d144d11432c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Jan 2025 21:07:32 +1100 Subject: [PATCH 170/175] Added Sphinx configuration key --- .readthedocs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index def6282dd56..3e03c76ea9b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,8 @@ version: 2 +sphinx: + configuration: docs/conf.py + formats: [pdf] build: From 2ce2ff297c9d3577e3bcc8f723107f661590afd2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Jan 2025 20:37:26 +1100 Subject: [PATCH 171/175] Test Python 3.14 pre-release --- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index d905a392585..b76b00eaa88 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] timeout-minutes: 30 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83a696f5f8b..e3efe0b593a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: ] python-version: [ "pypy3.10", + "3.14", "3.13t", "3.13", "3.12", From 0f2c554c698266ec0ba464f21a616cd0eda9a7fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Jan 2025 21:03:20 +1100 Subject: [PATCH 172/175] Improved comment --- Tests/oss-fuzz/test_fuzzers.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 90eb8713a8b..50d1467fc69 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -32,21 +32,18 @@ def test_fuzz_images(path: str) -> None: fuzzers.fuzz_image(f.read()) assert True except ( + # Known exceptions from Pillow OSError, SyntaxError, MemoryError, ValueError, NotImplementedError, OverflowError, - ): - # Known exceptions that are through from Pillow - assert True - except ( + # Known Image.* exceptions Image.DecompressionBombError, Image.DecompressionBombWarning, UnidentifiedImageError, ): - # Known Image.* exceptions assert True finally: fuzzers.disable_decompressionbomb_error() From cf438c53eed627e62baff752cb874aad3bcf63d5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Jan 2025 21:04:08 +1100 Subject: [PATCH 173/175] Removed UnidentifiedImageError, as it inherits from OSError --- Tests/oss-fuzz/test_fuzzers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 50d1467fc69..e42ec90aa54 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -7,7 +7,7 @@ import packaging import pytest -from PIL import Image, UnidentifiedImageError, features +from PIL import Image, features from Tests.helper import skip_unless_feature if sys.platform.startswith("win32"): @@ -42,7 +42,6 @@ def test_fuzz_images(path: str) -> None: # Known Image.* exceptions Image.DecompressionBombError, Image.DecompressionBombWarning, - UnidentifiedImageError, ): assert True finally: From e70c821436763e09813f0f53c76aaa3bb4fa76f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Jan 2025 20:57:49 +1100 Subject: [PATCH 174/175] Removed miniconda CPPFLAGS --- .ci/build.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/.ci/build.sh b/.ci/build.sh index e678f68ec85..ae10cb67155 100755 --- a/.ci/build.sh +++ b/.ci/build.sh @@ -3,8 +3,5 @@ set -e python3 -m coverage erase -if [ $(uname) == "Darwin" ]; then - export CPPFLAGS="-I/usr/local/miniconda/include"; -fi make clean make install-coverage From c67ed4678bf16be8630561ce0eb49eb0a2d3be40 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 16 Jan 2025 23:48:44 +1100 Subject: [PATCH 175/175] Moved strings inside debug statement Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/TiffImagePlugin.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index a246994efee..c871342fc50 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2202,11 +2202,15 @@ def fixIFD(self) -> None: if tag in self.Tags: cur_pos = self.f.tell() - tagname = TiffTags.lookup(tag).name - typname = TYPES.get(field_type, "unknown") - msg = f"fixIFD: {tagname} ({tag}) - type: {typname} ({field_type})" - msg += f"- type size: {field_size} - count: {count}" - logger.debug(msg) + logger.debug( + "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d", + TiffTags.lookup(tag).name, + tag, + TYPES.get(field_type, "unknown"), + field_type, + field_size, + count, + ) if is_local: self._fixOffsets(count, field_size)