diff --git a/.coveragerc b/.coveragerc index a94a25678..1f474015e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -18,6 +18,5 @@ exclude_also = [run] omit = - Tests/32bit_segfault_check.py - Tests/check_*.py + checks/*.py Tests/createfontdatachunk.py diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5384a74c0..d761d93b6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,42 +1,98 @@ #!/bin/bash -# Setup that needs to be done before multibuild utils are invoked -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). +# Safety check - Pillow builds require that CIBW_ARCHS is set, and that it only +# contains a single value (even though cibuildwheel allows multiple values in +# CIBW_ARCHS). This check doesn't work on Linux because of how the CIBW_ARCHS +# variable is exposed. +function check_cibw_archs { if [[ -z "$CIBW_ARCHS" ]]; then - echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." + echo "ERROR: Pillow 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." + echo "ERROR: Pillow builds only support a single architecture in CIBW_ARCHS." exit 1 fi +} +# Setup that needs to be done before multibuild utils are invoked. Process +# potential cross-build platforms before native platforms to ensure that we pick +# up the cross environment. +PROJECTDIR=$(pwd) +if [[ "$CIBW_PLATFORM" == "ios" ]]; then + check_cibw_archs + # On iOS, CIBW_ARCHS is actually a multi-arch - arm64_iphoneos, + # arm64_iphonesimulator or x86_64_iphonesimulator. Split into the CPU + # platform, and the iOS SDK. + PLAT=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\1/") + IOS_SDK=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\2/") + + # Build iOS builds in `build/iphoneos` or `build/iphonesimulator` + # (depending on the build target). Install them into `build/deps/iphoneos` + # or `build/deps/iphonesimulator` + WORKDIR=$(pwd)/build/$IOS_SDK + BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK + PATCH_DIR=$(pwd)/patches/iOS + + # GNU tooling insists on using aarch64 rather than arm64 + if [[ $PLAT == "arm64" ]]; then + GNU_ARCH=aarch64 + else + GNU_ARCH=x86_64 + fi + + IOS_SDK_PATH=$(xcrun --sdk $IOS_SDK --show-sdk-path) + CMAKE_SYSTEM_NAME=iOS + IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET + if [[ "$IOS_SDK" == "iphonesimulator" ]]; then + IOS_HOST_TRIPLE=$IOS_HOST_TRIPLE-simulator + fi + + # GNU Autotools doesn't recognize the existence of arm64-apple-ios-simulator + # as a valid host. However, the only difference between arm64-apple-ios and + # arm64-apple-ios-simulator is the choice of sysroot, and that is + # coordinated by CC, CFLAGS etc. From the perspective of configure, the two + # platforms are identical, so we can use arm64-apple-ios consistently. + # This (mostly) avoids us needing to patch config.sub in dependency sources. + HOST_CONFIGURE_FLAGS="--disable-shared --enable-static --host=$GNU_ARCH-apple-ios --build=$GNU_ARCH-apple-darwin" + + # CMake has native support for iOS. However, most of that support is based + # on using the Xcode builder, which isn't very helpful for most of Pillow's + # dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS + # etc. to ensure the right sysroot is selected. + HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO" + + # Meson needs to be pointed at a cross-platform configuration file + # This will be generated once CC etc. have been evaluated. + HOST_MESON_FLAGS="--cross-file $WORKDIR/meson-cross.txt -Dprefer_static=true -Ddefault_library=static" + +elif [[ "$(uname -s)" == "Darwin" ]]; then + check_cibw_archs # Build macOS dependencies in `build/darwin` # Install them into `build/deps/darwin` + PLAT=$CIBW_ARCHS WORKDIR=$(pwd)/build/darwin BUILD_PREFIX=$(pwd)/build/deps/darwin else # Build prefix will default to /usr/local + PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" WORKDIR=$(pwd)/build MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_VER=${AUDITWHEEL_POLICY:9} fi -PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" # Define custom utilities source wheels/multibuild/common_utils.sh source wheels/multibuild/library_builders.sh -if [ -z "$IS_MACOS" ]; then +if [[ -z "$IS_MACOS" ]]; then source wheels/multibuild/manylinux_utils.sh fi ARCHIVE_SDIR=pillow-depends-main -# Package versions for fresh source builds +# Package versions for fresh source builds. Version numbers with "Patched" +# annotations have a source code patch that is required for some platforms. If +# you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 LIBPNG_VERSION=1.6.49 @@ -47,32 +103,58 @@ TIFF_VERSION=4.7.0 LCMS2_VERSION=2.17 ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.4 -LIBWEBP_VERSION=1.5.0 +LIBWEBP_VERSION=1.5.0 # Patched; next release won't need patching. See patch file. BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 -BROTLI_VERSION=1.1.0 +BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file. LIBAVIF_VERSION=1.3.0 function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi - # This essentially duplicates the Homebrew recipe - CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ + # This essentially duplicates the Homebrew recipe. + # On iOS, we need a binary that can be executed on the build machine; but we + # can create a host-specific pc-path to store iOS .pc files. To ensure a + # macOS-compatible build, we temporarily clear environment flags that set + # iOS-specific values. + if [[ -n "$IOS_SDK" ]]; then + ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS + ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET + unset HOST_CONFIGURE_FLAGS + unset IPHONEOS_DEPLOYMENT_TARGET + fi + + CFLAGS="$CFLAGS -Wno-int-conversion" CPPFLAGS="" 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 + + if [[ -n "$IOS_SDK" ]]; then + HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS + IPHONEOS_DEPLOYMENT_TARGET=$ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET + fi; + export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config touch pkg-config-stamp } function build_zlib_ng { if [ -e zlib-stamp ]; then return; fi + # zlib-ng uses a "configure" script, but it's not a GNU autotools script, so + # it doesn't honor the usual flags. Temporarily disable any + # cross-compilation flags. + ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS + unset HOST_CONFIGURE_FLAGS + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat - if [ -n "$IS_MACOS" ]; then + HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS + + if [[ -n "$IS_MACOS" ]] && [[ -z "$IOS_SDK" ]]; then # Ensure that on macOS, the library name is an absolute path, not an # @rpath, so that delocate picks up the right library (and doesn't need # DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an - # option to control the install_name. + # option to control the install_name. This isn't needed on iOS, as iOS + # only builds the static library. install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib fi touch zlib-stamp @@ -82,7 +164,7 @@ function build_brotli { if [ -e brotli-stamp ]; then return; fi 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 $HOST_CMAKE_FLAGS . \ && make install) touch brotli-stamp } @@ -93,7 +175,7 @@ function build_harfbuzz { 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=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) + && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled $HOST_MESON_FLAGS) (cd $out_dir/build \ && meson install) touch harfbuzz-stamp @@ -164,19 +246,19 @@ function build { fi build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto - if [ -n "$IS_MACOS" ]; then + if [[ -n "$IS_MACOS" ]]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto 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 + 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 build_libjpeg_turbo - if [ -n "$IS_MACOS" ]; then + 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, + # headers/libs in the custom macOS/iOS 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 \ @@ -186,7 +268,10 @@ function build { build_tiff fi - build_libavif + if [[ -z "$IOS_SDK" ]]; then + # Short term workaround; don't build libavif on iOS + build_libavif + fi build_libpng build_lcms2 build_openjpeg @@ -201,14 +286,44 @@ function build { build_brotli - if [ -n "$IS_MACOS" ]; then + 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 else build_freetype fi - build_harfbuzz + if [[ -z "$IOS_SDK" ]]; then + # On iOS, there's no vendor-provided raqm, and we can't ship it due to + # licensing, so there's no point building harfbuzz. + build_harfbuzz + fi +} + +function create_meson_cross_config { + cat << EOF > $WORKDIR/meson-cross.txt +[binaries] +pkg-config = '$BUILD_PREFIX/bin/pkg-config' +cmake = '$(which cmake)' +c = '$CC' +cpp = '$CXX' +strip = '$STRIP' + +[built-in options] +c_args = '$CFLAGS -I$BUILD_PREFIX/include' +cpp_args = '$CXXFLAGS -I$BUILD_PREFIX/include' +c_link_args = '$CFLAGS -L$BUILD_PREFIX/lib' +cpp_link_args = '$CFLAGS -L$BUILD_PREFIX/lib' + +[host_machine] +system = 'darwin' +subsystem = 'ios' +kernel = 'xnu' +cpu_family = '$(uname -m)' +cpu = '$(uname -m)' +endian = 'little' + +EOF } # Perform all dependency builds in the build subfolder. @@ -227,24 +342,40 @@ if [[ ! -d $WORKDIR/pillow-depends-main ]]; then fi if [[ -n "$IS_MACOS" ]]; then - # 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 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 - # Ensure the basic structure of the build prefix directory exists. mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" - # Ensure pkg-config is available + # Ensure pkg-config is available. This is done *before* setting CC, CFLAGS + # etc. to ensure that the build is *always* a macOS build, even when building + # for iOS. build_pkg_config - # Ensure cmake is available + + # Ensure cmake is available, and that the default prefix used by CMake is + # the build prefix python3 -m pip install cmake + export CMAKE_PREFIX_PATH=$BUILD_PREFIX + + if [[ -n "$IOS_SDK" ]]; then + export AR="$(xcrun --find --sdk $IOS_SDK ar)" + export CPP="$(xcrun --find --sdk $IOS_SDK clang) -E" + export CC=$(xcrun --find --sdk $IOS_SDK clang) + export CXX=$(xcrun --find --sdk $IOS_SDK clang++) + export LD=$(xcrun --find --sdk $IOS_SDK ld) + export STRIP=$(xcrun --find --sdk $IOS_SDK strip) + + CPPFLAGS="$CPPFLAGS --sysroot=$IOS_SDK_PATH" + CFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" + CXXFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" + + # Having IPHONEOS_DEPLOYMENT_TARGET in the environment causes problems + # with some cross-building toolchains, because it introduces implicit + # behavior into clang. + unset IPHONEOS_DEPLOYMENT_TARGET + + # Now that we know CC etc., we can create a meson cross-configuration file + create_meson_cross_config + fi fi wrap_wheel_builder build diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index 9f5561c46..e6453d091 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -15,15 +15,12 @@ if (Test-Path $venv\Scripts\pypy.exe) { $python = "python.exe" } & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f -if ("$venv" -like "*\cibw-run-*-win_amd64\*") { - & $venv\Scripts\$python -m pip install numpy -} cd $pillow & $venv\Scripts\$python -VV if (!$?) { exit $LASTEXITCODE } & $venv\Scripts\$python selftest.py if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\$python -m pytest -vv -x Tests\check_wheel.py +& $venv\Scripts\$python -m pytest -vv -x checks\check_wheel.py if (!$?) { exit $LASTEXITCODE } & $venv\Scripts\$python -m pytest -vv -x Tests if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 94dbb4679..d73b6be58 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -25,8 +25,6 @@ else yum install -y fribidi fi -python3 -m pip install numpy - if [ ! -d "test-images-main" ]; then curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip unzip pillow-test-images.zip @@ -35,5 +33,5 @@ fi # Runs tests python3 selftest.py -python3 -m pytest -vv -x Tests/check_wheel.py +python3 -m pytest -vv -x checks/check_wheel.py python3 -m pytest -vv -x diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 16c350a14..52a3f2cdb 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -51,40 +51,60 @@ jobs: matrix: include: - name: "macOS 10.10 x86_64" + platform: macos os: macos-13 cibw_arch: x86_64 build: "cp3{9,10,11}*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" + platform: macos os: macos-13 cibw_arch: x86_64 build: "cp3{12,13,14}*" macosx_deployment_target: "10.13" - name: "macOS 10.15 x86_64" + platform: macos os: macos-13 cibw_arch: x86_64 build: "pp3*" macosx_deployment_target: "10.15" - name: "macOS arm64" + platform: macos os: macos-latest cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" + platform: linux os: ubuntu-latest cibw_arch: x86_64 - name: "manylinux_2_28 x86_64" + platform: linux os: ubuntu-latest cibw_arch: x86_64 build: "*manylinux*" manylinux: "manylinux_2_28" - name: "manylinux2014 and musllinux aarch64" + platform: linux os: ubuntu-24.04-arm cibw_arch: aarch64 - name: "manylinux_2_28 aarch64" + platform: linux os: ubuntu-24.04-arm cibw_arch: aarch64 build: "*manylinux*" manylinux: "manylinux_2_28" + - name: "iOS arm64 device" + platform: ios + os: macos-latest + cibw_arch: arm64_iphoneos + - name: "iOS arm64 simulator" + platform: ios + os: macos-latest + cibw_arch: arm64_iphonesimulator + - name: "iOS x86_64 simulator" + platform: ios + os: macos-13 + cibw_arch: x86_64_iphonesimulator steps: - uses: actions/checkout@v4 with: @@ -103,6 +123,7 @@ jobs: run: | python3 -m cibuildwheel --output-dir wheelhouse env: + CIBW_PLATFORM: ${{ matrix.platform }} CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy @@ -114,7 +135,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} + name: dist-${{ matrix.name }} path: ./wheelhouse/*.whl windows: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6abb732bb..75c7d3632 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.12.0 + rev: v0.12.2 hooks: - id: ruff-check args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.8.5 + rev: 1.8.6 hooks: - id: bandit args: [--severity-level=high] @@ -21,10 +21,10 @@ repos: rev: v1.5.5 hooks: - id: remove-tabs - exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$) - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.6 + rev: v20.1.7 hooks: - id: clang-format types: [c] @@ -46,19 +46,19 @@ repos: - id: check-yaml args: [--allow-multiple-documents] - id: end-of-file-fixer - exclude: ^Tests/images/ + exclude: ^Tests/images/|\.patch$ - id: trailing-whitespace - exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ + exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$ - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.1 + rev: 0.33.2 hooks: - id: check-github-workflows - id: check-readthedocs - id: check-renovate - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.9.0 + rev: v1.11.0 hooks: - id: zizmor diff --git a/MANIFEST.in b/MANIFEST.in index 48085b82e..95a6b1b92 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,8 @@ include LICENSE include Makefile include tox.ini graft Tests +graft checks +graft patches graft src graft depends graft winbuild diff --git a/Makefile b/Makefile index ce780acbc..6e050c715 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ debug: .PHONY: release-test release-test: - python3 Tests/check_release_notes.py + python3 checks/check_release_notes.py python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests diff --git a/Tests/helper.py b/Tests/helper.py index e71b4665b..34e4d6e75 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -271,17 +271,13 @@ def _cached_hopper(mode: str) -> Image.Image: im = hopper("L") else: im = hopper() - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning, match="BGR;"): - im = im.convert(mode) - else: - try: - im = im.convert(mode) - except ImportError: - if mode == "LAB": - im = Image.open("Tests/images/hopper.Lab.tif") - else: - raise + try: + im = im.convert(mode) + except ImportError: + if mode == "LAB": + im = Image.open("Tests/images/hopper.Lab.tif") + else: + raise return im diff --git a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff index b72509cc4..2ebb287e2 100644 Binary files a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff and b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff differ diff --git a/Tests/images/no_palette_with_transparency_after_rgb.gif b/Tests/images/no_palette_with_transparency_after_rgb.gif new file mode 100644 index 000000000..41357c147 Binary files /dev/null and b/Tests/images/no_palette_with_transparency_after_rgb.gif differ diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index e42ec90aa..37d11e0ba 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -10,8 +10,9 @@ import pytest from PIL import Image, features from Tests.helper import skip_unless_feature -if sys.platform.startswith("win32"): - pytest.skip("Fuzzer is linux only", allow_module_level=True) +if sys.platform.startswith("win32") or sys.platform == "ios": + pytest.skip("Fuzzer doesn't run on Windows or iOS", allow_module_level=True) + libjpeg_turbo_version = features.version("libjpeg_turbo") if libjpeg_turbo_version is not None: version = packaging.version.parse(libjpeg_turbo_version) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 88479ff0d..1e98ecfff 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -9,9 +9,9 @@ from PIL import _deprecate "version, expected", [ ( - 12, - "Old thing is deprecated and will be removed in Pillow 12 " - r"\(2025-10-15\)\. Use new thing instead\.", + 13, + "Old thing is deprecated and will be removed in Pillow 13 " + r"\(2026-10-15\)\. Use new thing instead\.", ), ( None, @@ -53,18 +53,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None: def test_plural() -> None: expected = ( - r"Old things are deprecated and will be removed in Pillow 12 \(2025-10-15\)\. " + r"Old things are deprecated and will be removed in Pillow 13 \(2026-10-15\)\. " r"Use new thing instead\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old things", 12, "new thing", plural=True) + _deprecate.deprecate("Old things", 13, "new thing", plural=True) def test_replacement_and_action() -> None: expected = "Use only one of 'replacement' and 'action'" with pytest.raises(ValueError, match=expected): _deprecate.deprecate( - "Old thing", 12, replacement="new thing", action="Upgrade to new thing" + "Old thing", 13, replacement="new thing", action="Upgrade to new thing" ) @@ -77,16 +77,16 @@ def test_replacement_and_action() -> None: ) def test_action(action: str) -> None: expected = ( - r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)\. " + r"Old thing is deprecated and will be removed in Pillow 13 \(2026-10-15\)\. " r"Upgrade to new thing\." ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 12, action=action) + _deprecate.deprecate("Old thing", 13, action=action) def test_no_replacement_or_action() -> None: expected = ( - r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)" + r"Old thing is deprecated and will be removed in Pillow 13 \(2026-10-15\)" ) with pytest.warns(DeprecationWarning, match=expected): - _deprecate.deprecate("Old thing", 12) + _deprecate.deprecate("Old thing", 13) diff --git a/Tests/test_features.py b/Tests/test_features.py index d06fb4d84..520c25b46 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -55,21 +55,6 @@ def test_version() -> None: test(feature, features.version_feature) -def test_webp_transparency() -> None: - with pytest.warns(DeprecationWarning, match="transp_webp"): - assert (features.check("transp_webp") or False) == features.check_module("webp") - - -def test_webp_mux() -> None: - with pytest.warns(DeprecationWarning, match="webp_mux"): - assert (features.check("webp_mux") or False) == features.check_module("webp") - - -def test_webp_anim() -> None: - with pytest.warns(DeprecationWarning, match="webp_anim"): - assert (features.check("webp_anim") or False) == features.check_module("webp") - - @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: version = features.version("libjpeg_turbo") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 98ac63d63..8b606291c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -101,6 +101,18 @@ def test_l_mode_after_rgb() -> None: assert im.mode == "RGB" +def test_l_mode_transparency_after_rgb() -> None: + with Image.open("Tests/images/no_palette_with_transparency_after_rgb.gif") as im: + expected = im.convert("RGB") + d = ImageDraw.Draw(expected) + d.rectangle([(0, 0), (64, 128)], fill="#000") + + im.seek(1) + assert im.mode == "RGB" + + assert_image_equal(im, expected) + + def test_palette_not_needed_for_second_frame() -> None: with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: im.seek(1) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 8ff59161f..b9b818506 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -93,21 +93,11 @@ def test_sizes() -> None: with Image.open(TEST_FILE) as im: assert isinstance(im, IcnsImagePlugin.IcnsImageFile) for w, h, r in im.info["sizes"]: - wr = w * r - hr = h * r - with pytest.warns( - DeprecationWarning, match=r"Setting size to \(width, height, scale\)" - ): - im.size = (w, h, r) - im.load() - assert im.mode == "RGBA" - assert im.size == (wr, hr) - # Test using load() with scale im.size = (w, h) im.load(scale=r) assert im.mode == "RGBA" - assert im.size == (wr, hr) + assert im.size == (w * r, h * r) # Check that we cannot load an incorrect size with pytest.raises(ValueError): diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 0ac325d91..3c4c892c8 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,11 +1,8 @@ from __future__ import annotations -import sys -from io import BytesIO, StringIO +from io import BytesIO -import pytest - -from PIL import Image, IptcImagePlugin +from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags from .helper import assert_image_equal, hopper @@ -78,13 +75,19 @@ def test_getiptcinfo_zero_padding() -> None: def test_getiptcinfo_tiff() -> None: - # Arrange + expected = {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"} + with Image.open("Tests/images/hopper.Lab.tif") as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) - # Assert - assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"} + assert iptc == expected + + # Test with LONG tag type + with Image.open("Tests/images/hopper.Lab.tif") as im: + im.tag_v2.tagtype[TiffImagePlugin.IPTC_NAA_CHUNK] = TiffTags.LONG + iptc = IptcImagePlugin.getiptcinfo(im) + + assert iptc == expected def test_getiptcinfo_tiff_none() -> None: @@ -95,35 +98,3 @@ def test_getiptcinfo_tiff_none() -> None: # Assert assert iptc is None - - -def test_i() -> None: - # Arrange - c = b"a" - - # Act - with pytest.warns(DeprecationWarning, match="IptcImagePlugin.i"): - ret = IptcImagePlugin.i(c) - - # Assert - assert ret == 97 - - -def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: - # Arrange - c = b"abc" - # Temporarily redirect stdout - mystdout = StringIO() - monkeypatch.setattr(sys, "stdout", mystdout) - - # Act - with pytest.warns(DeprecationWarning, match="IptcImagePlugin.dump"): - IptcImagePlugin.dump(c) - - # Assert - assert mystdout.getvalue() == "61 62 63 \n" - - -def test_pad_deprecation() -> None: - with pytest.warns(DeprecationWarning, match="IptcImagePlugin.PAD"): - assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 6dab418bf..08e879807 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -611,6 +611,24 @@ class TestFileJpeg: None ) ] + + for quality in range(101): + qtable_from_qtable_quality = self.roundtrip( + im, + qtables={0: standard_l_qtable, 1: standard_chrominance_qtable}, + quality=quality, + ).quantization + + qtable_from_quality = self.roundtrip(im, quality=quality).quantization + + if features.check_feature("libjpeg_turbo"): + assert qtable_from_qtable_quality == qtable_from_quality + else: + assert qtable_from_qtable_quality[0] == qtable_from_quality[0] + assert ( + qtable_from_qtable_quality[1][1:] == qtable_from_quality[1][1:] + ) + # list of qtable lists assert_image_similar( im, @@ -1097,14 +1115,6 @@ class TestFileJpeg: assert im._repr_jpeg_() is None - def test_deprecation(self) -> None: - with Image.open(TEST_FILE) as im: - assert isinstance(im, JpegImagePlugin.JpegImageFile) - with pytest.warns(DeprecationWarning, match="huffman_ac"): - assert im.huffman_ac == {} - with pytest.warns(DeprecationWarning, match="huffman_dc"): - assert im.huffman_dc == {} - @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1ec39eba5..c245a5a9b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -256,19 +256,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, tiffinfo=new_ifd) - @pytest.mark.parametrize( - "libtiff", - ( - pytest.param( - True, - marks=pytest.mark.skipif( - not getattr(Image.core, "libtiff_support_custom_tags", False), - reason="Custom tags not supported by older libtiff", - ), - ), - False, - ), - ) + @pytest.mark.parametrize("libtiff", (True, False)) def test_custom_metadata( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool ) -> None: @@ -724,8 +712,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) - if Image.core.libtiff_support_custom_tags: - assert reloaded.tag_v2[34665] == 125456 + assert reloaded.tag_v2[34665] == 125456 def test_crashing_metadata( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path @@ -777,19 +764,7 @@ class TestFileLibTiff(LibTiffTestCase): assert icc_libtiff is not None assert icc == icc_libtiff - @pytest.mark.parametrize( - "libtiff", - ( - pytest.param( - True, - marks=pytest.mark.skipif( - not getattr(Image.core, "libtiff_support_custom_tags", False), - reason="Custom tags not supported by older libtiff", - ), - ), - False, - ), - ) + @pytest.mark.parametrize("libtiff", (True, False)) def test_write_icc( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool ) -> None: diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index e11e6bb52..7e236028d 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -27,6 +27,6 @@ def test_valid_file() -> None: # Assert assert im.format == "MCIDAS" - assert im.mode == "I" + assert im.mode == "I;16B" assert im.size == (1800, 400) assert_image_equal_tofile(im, saved_file) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 9bf247636..85dd5fc7f 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -359,13 +359,24 @@ def test_save_all_progress() -> None: def test_save_xmp() -> None: im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") + + def roundtrip_xmp() -> list[Any]: + im_reloaded = roundtrip(im, xmp=b"Default", save_all=True, append_images=[im2]) + xmp = [im_reloaded.info["xmp"]] + im_reloaded.seek(1) + return xmp + [im_reloaded.info["xmp"]] + + # Use the save parameters for all frames by default + assert roundtrip_xmp() == [b"Default", b"Default"] + + # Specify a value for the first frame + im.encoderinfo = {"xmp": b"First frame"} + assert roundtrip_xmp() == [b"First frame", b"Default"] + del im.encoderinfo + + # Specify value for the second frame im2.encoderinfo = {"xmp": b"Second frame"} - im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) + assert roundtrip_xmp() == [b"Default", b"Second frame"] # Test that encoderinfo is unchanged assert im2.encoderinfo == {"xmp": b"Second frame"} - - assert im_reloaded.info["xmp"] == b"First frame" - - im_reloaded.seek(1) - assert im_reloaded.info["xmp"] == b"Second frame" diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index b60a61112..0cf07ab33 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -681,16 +681,21 @@ class TestFileTiff: assert im.tag_v2[278] == 256 im = hopper() + im.encoderinfo = {"tiffinfo": {278: 100}} im2 = Image.new("L", (128, 128)) - im2.encoderinfo = {"tiffinfo": {278: 256}} - im.save(outfile, save_all=True, append_images=[im2]) + im3 = im2.copy() + im3.encoderinfo = {"tiffinfo": {278: 300}} + im.save(outfile, save_all=True, tiffinfo={278: 200}, append_images=[im2, im3]) with Image.open(outfile) as im: assert isinstance(im, TiffImagePlugin.TiffImageFile) - assert im.tag_v2[278] == 128 + assert im.tag_v2[278] == 100 im.seek(1) - assert im.tag_v2[278] == 256 + assert im.tag_v2[278] == 200 + + im.seek(2) + assert im.tag_v2[278] == 300 def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" diff --git a/Tests/test_image.py b/Tests/test_image.py index 069083b19..83b027aa2 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -30,7 +30,6 @@ from .helper import ( assert_image_similar_tofile, assert_not_all_same, hopper, - is_big_endian, is_win32, mark_if_feature_version, skip_unless_feature, @@ -50,19 +49,10 @@ except ImportError: PrettyPrinter = None -# Deprecation helper -def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning, match="BGR;"): - return Image.new(mode, size) - else: - return Image.new(mode, size) - - class TestImage: - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_image_modes_success(self, mode: str) -> None: - helper_image_new(mode, (1, 1)) + Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode: str) -> None: @@ -160,6 +150,10 @@ class TestImage: with pytest.raises(AttributeError): im.mode = "P" # type: ignore[misc] + def test_empty_path(self) -> None: + with pytest.raises(FileNotFoundError): + Image.open("") + def test_invalid_image(self) -> None: im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): @@ -1138,39 +1132,29 @@ class TestImage: assert len(caplog.records) == 0 assert im.fp is None - def test_deprecation(self) -> None: - with pytest.warns(DeprecationWarning, match="Image.isImageType"): - assert not Image.isImageType(None) - class TestImageBytes: - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_roundtrip_bytes_constructor(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() - if mode.startswith("BGR;"): - with pytest.warns(DeprecationWarning, match=mode): - reloaded = Image.frombytes(mode, im.size, source_bytes) - else: - reloaded = Image.frombytes(mode, im.size, source_bytes) + reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_roundtrip_bytes_method(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() - reloaded = helper_image_new(mode, im.size) + reloaded = Image.new(mode, im.size) reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) + @pytest.mark.parametrize("mode", Image.MODES) def test_getdata_putdata(self, mode: str) -> None: - if is_big_endian() and mode == "BGR;15": - pytest.xfail("Known failure of BGR;15 on big-endian") im = hopper(mode) - reloaded = helper_image_new(mode, im.size) + reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 66412a035..b3de5c13d 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -123,10 +123,6 @@ class TestImageGetPixel: bands = Image.getmodebands(mode) if bands == 1: return 1 - if mode in ("BGR;15", "BGR;16"): - # These modes have less than 8 bits per band, - # so (1, 2, 3) cannot be roundtripped. - return (16, 32, 49) return tuple(range(1, bands + 1)) def check(self, mode: str, expected_color_int: int | None = None) -> None: @@ -191,11 +187,6 @@ class TestImageGetPixel: def test_basic(self, mode: str) -> None: self.check(mode) - @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) - def test_deprecated(self, mode: str) -> None: - with pytest.warns(DeprecationWarning, match="BGR;"): - self.check(mode) - def test_list(self) -> None: im = hopper() assert im.getpixel([0, 0]) == (20, 20, 70) @@ -218,7 +209,7 @@ class TestImageGetPixel: class TestImagePutPixelError: - IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] + IMAGE_MODES1 = ["LA", "RGB", "RGBA"] IMAGE_MODES2 = ["L", "I", "I;16"] INVALID_TYPES = ["foo", 1.0, None] @@ -234,11 +225,6 @@ class TestImagePutPixelError: ( ("L", (0, 2), "color must be int or single-element tuple"), ("LA", (0, 3), "color must be int, or tuple of one or two elements"), - ( - "BGR;15", - (0, 2), - "color must be int, or tuple of one or three elements", - ), ( "RGB", (0, 2, 5), diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index 7b5f7a589..07612e587 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,7 +1,5 @@ from __future__ import annotations -import pytest - from .helper import hopper @@ -10,10 +8,3 @@ def test_sanity() -> None: type_repr = repr(type(im.getim())) assert "PyCapsule" in type_repr - - with pytest.warns(DeprecationWarning, match="id property"): - assert isinstance(im.im.id, int) - - with pytest.warns(DeprecationWarning, match="unsafe_ptrs property"): - ptrs = dict(im.im.unsafe_ptrs) - assert ptrs.keys() == {"image8", "image32", "image"} diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 34c1763b8..bf8e89b53 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -78,16 +78,6 @@ def test_mode_F() -> None: assert list(im.getdata()) == target -@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode: str) -> None: - data = [(16, 32, 49), (32, 32, 98)] - with pytest.warns(DeprecationWarning, match=mode): - im = Image.new(mode, (1, 2)) - im.putdata(data) - - assert list(im.getdata()) == data - - def test_array_B() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index f700d20c0..270500a44 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -324,7 +324,7 @@ class TestImageResize: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) - @pytest.mark.parametrize("mode", ("1", "P", "BGR;15", "BGR;16")) + @pytest.mark.parametrize("mode", ("1", "P")) def test_default_filter_nearest(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index b6db0ab5c..55a4a87fb 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -54,10 +54,6 @@ def skip_missing() -> None: def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. - with pytest.warns(DeprecationWarning, match="PIL.ImageCms.versions"): - v = ImageCms.versions() # should return four strings - assert v[0] == "1.0.0 pil" - assert list(map(type, v)) == [str, str, str, str] # internal version number version = features.version_module("littlecms2") @@ -677,12 +673,6 @@ def test_auxiliary_channels_isolated() -> None: assert_image_equal(test_image.convert(dst_format[2]), reference_image) -def test_long_modes() -> None: - p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") - with pytest.warns(DeprecationWarning, match="ABCDEFGHI"): - ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI") - - @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) def test_rgb_lab(mode: str) -> None: im = Image.new(mode, (1, 1)) @@ -700,18 +690,3 @@ def test_cmyk_lab() -> None: im = Image.new("CMYK", (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (255, 128, 128) - - -def test_deprecation() -> None: - with pytest.warns(DeprecationWarning, match="ImageCms.DESCRIPTION"): - assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") - with pytest.warns(DeprecationWarning, match="ImageCms.VERSION"): - assert ImageCms.VERSION == "1.0.0 pil" - with pytest.warns(DeprecationWarning, match="ImageCms.FLAGS"): - assert isinstance(ImageCms.FLAGS, dict) - - profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) - with pytest.warns(DeprecationWarning, match="RGBA;16B"): - ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB") - with pytest.warns(DeprecationWarning, match="RGBA;16B"): - ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B") diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 881f9c85d..e1dcbc52c 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1732,8 +1732,3 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: draw.rectangle(xy) with pytest.raises(ValueError): draw.rounded_rectangle(xy) - - -def test_getdraw() -> None: - with pytest.warns(DeprecationWarning, match="'hints' parameter"): - ImageDraw.getdraw(None, []) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index a9444c26d..d4dfb1b6d 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -151,11 +151,6 @@ class TestImageFile: # Despite multiple tiles, assert only one tile caused a read of maxblock size assert reads.count(im.decodermaxblock) == 1 - def test_raise_oserror(self) -> None: - with pytest.warns(DeprecationWarning, match="raise_oserror"): - with pytest.raises(OSError): - ImageFile.raise_oserror(1) - def test_raise_typeerror(self) -> None: with pytest.raises(TypeError): parser = ImageFile.Parser() diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 9334d30e4..4565d35ba 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -11,7 +11,6 @@ from pathlib import Path from typing import Any, BinaryIO import pytest -from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features from PIL._typing import StrOrBytesPath @@ -691,16 +690,6 @@ def test_complex_font_settings() -> None: def test_variation_get(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.get_variation_names() - with pytest.raises(NotImplementedError): - font.get_variation_axes() - return - with pytest.raises(OSError): font.get_variation_names() with pytest.raises(OSError): @@ -763,14 +752,6 @@ def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_name("Bold") - return - with pytest.raises(OSError): font.set_variation_by_name("Bold") @@ -790,14 +771,6 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: - version = features.version_module("freetype2") - assert version is not None - freetype = parse_version(version) - if freetype < parse_version("2.9.1"): - with pytest.raises(NotImplementedError): - font.set_variation_by_axes([100]) - return - with pytest.raises(OSError): font.set_variation_by_axes([500, 50]) @@ -1209,15 +1182,3 @@ def test_invalid_truetype_sizes_raise_valueerror( ) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) - - -def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None: - # Arrange: mock features.version_module to return fake FreeType version - def fake_version_module(module: str) -> str: - return "2.9.0" - - monkeypatch.setattr(features, "version_module", fake_version_module) - - # Act / Assert - with pytest.warns(DeprecationWarning, match="FreeType 2.9.0"): - ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index eec76118a..26c04b9a0 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -2,8 +2,6 @@ from __future__ import annotations from typing import Any -import pytest - from PIL import Image, ImageMath @@ -55,11 +53,6 @@ def test_sanity() -> None: ) -def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning, match="ImageMath.lambda_eval options"): - assert ImageMath.lambda_eval(lambda args: 1, images) == 1 - - def test_ops() -> None: assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1" diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py index 60ad6aafa..5e141a55b 100644 --- a/Tests/test_imagemath_unsafe_eval.py +++ b/Tests/test_imagemath_unsafe_eval.py @@ -35,16 +35,6 @@ def test_sanity() -> None: assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3" -def test_eval_deprecated() -> None: - with pytest.warns(DeprecationWarning, match="ImageMath.eval"): - assert ImageMath.eval("1") == 1 - - -def test_options_deprecated() -> None: - with pytest.warns(DeprecationWarning, match="ImageMath.unsafe_eval options"): - assert ImageMath.unsafe_eval("1", images) == 1 - - def test_ops() -> None: assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1" assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2" diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 2d6af70eb..da6157d4e 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -361,18 +361,6 @@ class TestLibUnpack: "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) ) - def test_BGR(self) -> None: - with pytest.warns(DeprecationWarning, match="BGR;15"): - self.assert_unpack( - "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8) - ) - with pytest.warns(DeprecationWarning, match="BGR;16"): - self.assert_unpack( - "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) - ) - with pytest.warns(DeprecationWarning, match="BGR;24"): - self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - def test_RGBA(self) -> None: self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack( diff --git a/Tests/test_main.py b/Tests/test_main.py index 2582dbee3..65e7a44d8 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -7,6 +7,7 @@ import sys import pytest +@pytest.mark.skipif(sys.platform == "ios", reason="Processes not supported on iOS") @pytest.mark.parametrize( "args, report", ((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)), diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 8235daf32..a161d3f05 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,5 +1,7 @@ from __future__ import annotations +from importlib.metadata import metadata + import pytest from PIL import __version__ @@ -9,7 +11,7 @@ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") def test_pyroma() -> None: # Arrange - data = pyroma.projectdata.get_data(".") + data = pyroma.projectdata.map_metadata_keys(metadata("Pillow")) # Act rating = pyroma.ratings.rate(data) @@ -23,11 +25,5 @@ def test_pyroma() -> None: ) else: - # Should have a perfect score, but pyroma does not support PEP 639 yet. - assert rating == ( - 9, - [ - "Your package does neither have a license field " - "nor any license classifiers." - ], - ) + # Should have a perfect score + assert rating == (10, []) diff --git a/Tests/32bit_segfault_check.py b/checks/32bit_segfault_check.py similarity index 100% rename from Tests/32bit_segfault_check.py rename to checks/32bit_segfault_check.py diff --git a/Tests/check_fli_oob.py b/checks/check_fli_oob.py similarity index 100% rename from Tests/check_fli_oob.py rename to checks/check_fli_oob.py diff --git a/Tests/check_fli_overflow.py b/checks/check_fli_overflow.py similarity index 100% rename from Tests/check_fli_overflow.py rename to checks/check_fli_overflow.py diff --git a/Tests/check_icns_dos.py b/checks/check_icns_dos.py similarity index 100% rename from Tests/check_icns_dos.py rename to checks/check_icns_dos.py diff --git a/Tests/check_imaging_leaks.py b/checks/check_imaging_leaks.py similarity index 100% rename from Tests/check_imaging_leaks.py rename to checks/check_imaging_leaks.py diff --git a/Tests/check_j2k_dos.py b/checks/check_j2k_dos.py similarity index 100% rename from Tests/check_j2k_dos.py rename to checks/check_j2k_dos.py diff --git a/Tests/check_j2k_leaks.py b/checks/check_j2k_leaks.py similarity index 100% rename from Tests/check_j2k_leaks.py rename to checks/check_j2k_leaks.py diff --git a/Tests/check_j2k_overflow.py b/checks/check_j2k_overflow.py similarity index 100% rename from Tests/check_j2k_overflow.py rename to checks/check_j2k_overflow.py diff --git a/Tests/check_jp2_overflow.py b/checks/check_jp2_overflow.py similarity index 100% rename from Tests/check_jp2_overflow.py rename to checks/check_jp2_overflow.py diff --git a/Tests/check_jpeg_leaks.py b/checks/check_jpeg_leaks.py similarity index 99% rename from Tests/check_jpeg_leaks.py rename to checks/check_jpeg_leaks.py index 5f290c6cd..2f42ad734 100644 --- a/Tests/check_jpeg_leaks.py +++ b/checks/check_jpeg_leaks.py @@ -13,7 +13,7 @@ iterations = 5000 When run on a system without the jpeg leak fixes, the valgrind runs look like this. -valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py +valgrind --tool=massif python test-installed.py -s -v checks/check_jpeg_leaks.py """ diff --git a/Tests/check_large_memory.py b/checks/check_large_memory.py similarity index 100% rename from Tests/check_large_memory.py rename to checks/check_large_memory.py diff --git a/Tests/check_large_memory_numpy.py b/checks/check_large_memory_numpy.py similarity index 100% rename from Tests/check_large_memory_numpy.py rename to checks/check_large_memory_numpy.py diff --git a/Tests/check_libtiff_segfault.py b/checks/check_libtiff_segfault.py similarity index 100% rename from Tests/check_libtiff_segfault.py rename to checks/check_libtiff_segfault.py diff --git a/Tests/check_png_dos.py b/checks/check_png_dos.py similarity index 100% rename from Tests/check_png_dos.py rename to checks/check_png_dos.py diff --git a/Tests/check_release_notes.py b/checks/check_release_notes.py similarity index 100% rename from Tests/check_release_notes.py rename to checks/check_release_notes.py diff --git a/Tests/check_wheel.py b/checks/check_wheel.py similarity index 74% rename from Tests/check_wheel.py rename to checks/check_wheel.py index a78fb09b0..3d806eb71 100644 --- a/Tests/check_wheel.py +++ b/checks/check_wheel.py @@ -4,8 +4,7 @@ import platform import sys from PIL import features - -from .helper import is_pypy +from Tests.helper import is_pypy def test_wheel_modules() -> None: @@ -24,6 +23,11 @@ def test_wheel_modules() -> None: if platform.machine() == "ARM64": expected_modules.remove("avif") + elif sys.platform == "ios": + # tkinter is not available on iOS + # libavif is not available on iOS (for now) + expected_modules -= {"tkinter", "avif"} + assert set(features.get_supported_modules()) == expected_modules @@ -35,9 +39,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { - "webp_anim", - "webp_mux", - "transp_webp", "raqm", "fribidi", "harfbuzz", @@ -50,5 +51,9 @@ def test_wheel_features() -> None: expected_features.remove("xcb") elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm": expected_features.remove("zlib_ng") + elif sys.platform == "ios": + # Can't distribute raqm due to licensing, and there's no system version; + # fribidi and harfbuzz won't be available if raqm isn't available. + expected_features -= {"raqm", "fribidi", "harfbuzz"} assert set(features.get_supported_features()) == expected_features diff --git a/codecov.yml b/codecov.yml index 84920238f..c29b4bc90 100644 --- a/codecov.yml +++ b/codecov.yml @@ -16,6 +16,5 @@ coverage: # Matches 'omit:' in .coveragerc ignore: - - "Tests/32bit_segfault_check.py" - - "Tests/check_*.py" + - "checks/*.py" - "Tests/createfontdatachunk.py" diff --git a/docs/deprecations.rst b/docs/deprecations.rst index def98b80a..236554565 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,96 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a :py:exc:`DeprecationWarning` is issued. -ImageFile.raise_oserror -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.2.0 - -``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). The function is undocumented and is only useful for translating -error codes returned by a codec's ``decode()`` method, which ImageFile already does -automatically. - -IptcImageFile helper functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.2.0 - -The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant -``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow -12.0.0 (2025-10-15). These are undocumented helper functions intended -for internal use, so there is no replacement. They can each be replaced -by a single line of code using builtin functions in Python. - -ImageCms constants and versions() function -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 10.3.0 - -A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. -This includes a table of flags based on LittleCMS version 1 which has been -replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. - -============================================ ==================================================== -Deprecated Use instead -============================================ ==================================================== -``ImageCms.DESCRIPTION`` No replacement -``ImageCms.VERSION`` ``PIL.__version__`` -``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` -``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` -``ImageCms.FLAGS["MATRIXONLY"]`` No replacement -``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` -``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` -``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` -``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` -``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` -``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` -``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` -``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` -``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` -``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` -``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` -``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` -``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` -``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` -``ImageCms.versions()`` :py:func:`PIL.features.version_module` with - ``feature="littlecms2"``, :py:data:`sys.version` or - :py:data:`sys.version_info`, and ``PIL.__version__`` -============================================ ==================================================== - -ImageMath eval() -^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.3.0 - -``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or -:py:meth:`~PIL.ImageMath.unsafe_eval` instead. - -BGR;15, BGR 16 and BGR;24 -^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. - -Non-image modes in ImageCms -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow -image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped -is also deprecated. - -Support for LibTIFF earlier than 4 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 10.4.0 - -Support for LibTIFF earlier than version 4 has been deprecated. -Upgrade to a newer version of LibTIFF instead. - ImageDraw.getdraw hints parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -109,72 +19,6 @@ ImageDraw.getdraw hints parameter The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. -FreeType 2.9.0 -^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 -(2025-10-15), when FreeType 2.9.1 will be the minimum supported. - -We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). - -.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ - -ICNS (width, height, scale) sizes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -Setting an ICNS image size to ``(width, height, scale)`` before loading has been -deprecated. Instead, ``load(scale)`` can be used. - -Image isImageType() -^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` -instead. - -ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and -:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword -arguments can be used instead. - -JpegImageFile.huffman_ac and JpegImageFile.huffman_dc -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They -have been deprecated, and will be removed in Pillow 12 (2025-10-15). - -Specific WebP feature checks -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -``features.check("transp_webp")``, ``features.check("webp_mux")`` and -``features.check("webp_anim")`` are now deprecated. They will always return -``True`` if the WebP module is installed, until they are removed in Pillow -12.0.0 (2025-10-15). - -Get internal pointers to objects -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. deprecated:: 11.0.0 - -``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been -deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obtaining -raw pointers to ``ImagingCore`` internals. To interact with C code, you can use -``Image.Image.getim()``, which returns a ``Capsule`` object. - ExifTags.IFD.Makernote ^^^^^^^^^^^^^^^^^^^^^^ @@ -221,12 +65,185 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +ImageFile.raise_oserror +^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.2.0 +.. versionremoved:: 12.0.0 + +``ImageFile.raise_oserror()`` has been removed. The function was undocumented and was +only useful for translating error codes returned by a codec's ``decode()`` method, +which ImageFile already did automatically. + +IptcImageFile helper functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.2.0 +.. versionremoved:: 12.0.0 + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been removed. These were undocumented helper functions +intended for internal use, so there is no replacement. They can each be replaced by a +single line of code using builtin functions in Python. + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.3.0 +.. versionremoved:: 12.0.0 + +A number of constants and a function in :py:mod:`.ImageCms` have been removed. This +includes a table of flags based on LittleCMS version 1 which has been replaced with a +new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== + +ImageMath eval() +^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.3.0 +.. versionremoved:: 12.0.0 + +``ImageMath.eval()`` has been removed. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. + +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. + +Non-image modes in ImageCms +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow +image modes has been removed. Defaulting to "L" or "1" if the mode cannot be mapped has +also been removed. + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +Support for LibTIFF earlier than version 4 has been removed. +Upgrade to a newer version of LibTIFF instead. + +ImageDraw.getdraw hints parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.4.0 +.. versionremoved:: 12.0.0 + +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. + +FreeType 2.9.0 +^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +Support for FreeType 2.9.0 has been removed. FreeType 2.9.1 is the minimum version +supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + +ICNS (width, height, scale) sizes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +Setting an ICNS image size to ``(width, height, scale)`` before loading has been +removed. Instead, ``load(scale)`` can be used. + +Image isImageType() +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +``Image.isImageType(im)`` has been removed. Use ``isinstance(im, Image.Image)`` +instead. + +ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and +:py:meth:`~PIL.ImageMath.unsafe_eval()` has been removed. One or more keyword +arguments can be used instead. + +JpegImageFile.huffman_ac and JpegImageFile.huffman_dc +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They +have been removed. + +Specific WebP feature checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` have been removed. + +Get internal pointers to objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 +.. versionremoved:: 12.0.0 + +``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been +removed. They were used for obtaining raw pointers to ``ImagingCore`` internals. To +interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule`` +object. + TiffImagePlugin IFD_LEGACY_API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. versionremoved:: 11.0.0 -``TiffImagePlugin.IFD_LEGACY_API`` was removed, as it was an unused setting. +``TiffImagePlugin.IFD_LEGACY_API`` has been removed, as it was an unused setting. PSFile ~~~~~~ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 83df2bd5c..03ee96c0f 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -557,6 +557,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: hardly any gain in image quality. The value ``keep`` is only valid for JPEG files and will retain the original image quality level, subsampling, and qtables. + For more information on how qtables are modified based on the quality parameter, + see the qtables section. **optimize** If present and true, indicates that the encoder should make an extra pass @@ -622,6 +624,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: range(len(keys))) of lists of 64 integers. There must be between 2 and 4 tables. + If a quality parameter is provided, the qtables will be adjusted accordingly. + By default, the qtables are based on a standard JPEG table with a quality of 50. + The qtable values will be reduced if the quality is higher than 50 and increased + if the quality is lower than 50. + .. versionadded:: 2.5.0 **streamtype** diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index a56f94316..c2227f1d2 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -75,7 +75,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.2.1 |arm | +| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm | | +----------------------------+------------------+ | | | 3.8 | 10.4.0 | | +----------------------------------+----------------------------+------------------+--------------+ diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 238390e75..4a2123677 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -56,7 +56,6 @@ Functions .. autofunction:: get_display_profile .. autofunction:: isIntentSupported .. autofunction:: profileToProfile -.. autofunction:: versions CmsProfile ---------- diff --git a/docs/reference/arrow_support.rst b/docs/reference/arrow_support.rst index 063046d8c..8e8b86c8e 100644 --- a/docs/reference/arrow_support.rst +++ b/docs/reference/arrow_support.rst @@ -21,9 +21,7 @@ with any Arrow provider or consumer in the Python ecosystem. Data formats ============ -Pillow currently supports exporting Arrow images in all modes -**except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to -line-length packing in these modes making for non-continuous memory. +Pillow currently supports exporting Arrow images in all modes. For single-band images, the exported array is width*height elements, with each pixel corresponding to the appropriate Arrow type. diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 381d7830a..45067ba35 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -60,9 +60,6 @@ Support for the following features can be checked: * ``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. -* ``transp_webp``: Deprecated. Always ``True`` if WebP module is installed. -* ``webp_mux``: Deprecated. Always ``True`` if WebP module is installed. -* ``webp_anim``: Deprecated. Always ``True`` if WebP module is installed. .. autofunction:: PIL.features.check_feature .. autofunction:: PIL.features.version_feature diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst index 2d35d8228..409d50295 100644 --- a/docs/releasenotes/11.3.0.rst +++ b/docs/releasenotes/11.3.0.rst @@ -68,7 +68,15 @@ AVIF support in wheels ^^^^^^^^^^^^^^^^^^^^^^ Support for reading and writing AVIF images is now included in Pillow's wheels, except -for Windows ARM64. libaom is available as an encoder and dav1d as a decoder. +for Windows ARM64 and iOS. libaom is available as an encoder and dav1d as a decoder. +(Thank you Frankie Dintino and Andrew Murray!) + +iOS +^^^ + +Pillow now provides wheels that can be used on iOS ARM64 devices, and on the iOS +simulator on ARM64 and x86_64. Currently, only Python 3.13 wheels are available. +(Thank you Russell Keith-Magee and Andrew Murray!) Python 3.14 beta ^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst new file mode 100644 index 000000000..68b664443 --- /dev/null +++ b/docs/releasenotes/12.0.0.rst @@ -0,0 +1,140 @@ +12.0.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +ImageFile.raise_oserror +^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageFile.raise_oserror()`` has been removed. The function was undocumented and was +only useful for translating error codes returned by a codec's ``decode()`` method, +which ImageFile already did automatically. + +IptcImageFile helper functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been removed. These were undocumented helper functions +intended for internal use, so there is no replacement. They can each be replaced by a +single line of code using builtin functions in Python. + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A number of constants and a function in :py:mod:`.ImageCms` have been removed. This +includes a table of flags based on LittleCMS version 1 which has been replaced with a +new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== + +ImageMath eval() +^^^^^^^^^^^^^^^^ + +``ImageMath.eval()`` has been removed. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. + +BGR;15, BGR 16 and BGR;24 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The experimental BGR;15, BGR;16 and BGR;24 modes have been removed. + +Non-image modes in ImageCms +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow +image modes has been removed. Defaulting to "L" or "1" if the mode cannot be mapped has +also been removed. + +Support for LibTIFF earlier than 4 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support for LibTIFF earlier than version 4 has been removed. +Upgrade to a newer version of LibTIFF instead. + +ImageDraw.getdraw hints parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed. + +FreeType 2.9.0 +^^^^^^^^^^^^^^ + +Support for FreeType 2.9.0 has been removed. FreeType 2.9.1 is the minimum version +supported. + +We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). + +.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +TODO +^^^^ + +TODO + +Other changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index a85f1e075..f66240c89 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 + 12.0.0 11.3.0 11.2.1 11.1.0 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index a453d2a43..b603a9938 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -20,6 +20,8 @@ Backwards incompatible changes TODO ^^^^ +TODO + Deprecations ============ diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 000000000..ff4a8f099 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,14 @@ +Although we try to use official sources for dependencies, sometimes the official +sources don't support a platform (especially mobile platforms), or there's a bug +fix/feature that is required to support Pillow's usage. + +This folder contains patches that must be applied to official sources, organized +by the platforms that need those patches. + +Each patch is against the root of the unpacked official tarball, and is named by +appending `.patch` to the end of the tarball that is to be patched. This +includes the full version number; so if the version is bumped, the patch will +at a minimum require a filename change. + +Wherever possible, these patches should be contributed upstream, in the hope that +future Pillow versions won't need to maintain these patches. diff --git a/patches/iOS/brotli-1.1.0.tar.gz.patch b/patches/iOS/brotli-1.1.0.tar.gz.patch new file mode 100644 index 000000000..f165a9ac1 --- /dev/null +++ b/patches/iOS/brotli-1.1.0.tar.gz.patch @@ -0,0 +1,46 @@ +# Brotli 1.1.0 doesn't have explicit support for iOS as a CMAKE_SYSTEM_NAME. +# That release was from 2023; there have been subsequent changes that allow +# Brotli to build on iOS without any patches, as long as -DBROTLI_BUILD_TOOLS=NO +# is specified on the command line. +# +diff -ru brotli-1.1.0-orig/CMakeLists.txt brotli-1.1.0/CMakeLists.txt +--- brotli-1.1.0-orig/CMakeLists.txt 2023-08-29 19:00:29 ++++ brotli-1.1.0/CMakeLists.txt 2024-11-07 10:46:26 +@@ -114,6 +114,8 @@ + add_definitions(-DOS_MACOSX) + set(CMAKE_MACOS_RPATH TRUE) + set(CMAKE_INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/lib") ++elseif(${CMAKE_SYSTEM_NAME} MATCHES "iOS") ++ add_definitions(-DOS_IOS) + endif() + + if(BROTLI_EMSCRIPTEN) +@@ -174,10 +176,12 @@ + + # Installation + if(NOT BROTLI_BUNDLED_MODE) +- install( +- TARGETS brotli +- RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" +- ) ++ if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "iOS") ++ install( ++ TARGETS brotli ++ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" ++ ) ++ endif() + + install( + TARGETS ${BROTLI_LIBRARIES_CORE} +diff -ru brotli-1.1.0-orig/c/common/platform.h brotli-1.1.0/c/common/platform.h +--- brotli-1.1.0-orig/c/common/platform.h 2023-08-29 19:00:29 ++++ brotli-1.1.0/c/common/platform.h 2024-11-07 10:47:28 +@@ -33,7 +33,7 @@ + #include + #elif defined(OS_FREEBSD) + #include +-#elif defined(OS_MACOSX) ++#elif defined(OS_MACOSX) || defined(OS_IOS) + #include + /* Let's try and follow the Linux convention */ + #define BROTLI_X_BYTE_ORDER BYTE_ORDER diff --git a/patches/iOS/libwebp-1.5.0.tar.gz.patch b/patches/iOS/libwebp-1.5.0.tar.gz.patch new file mode 100644 index 000000000..fefb72b68 --- /dev/null +++ b/patches/iOS/libwebp-1.5.0.tar.gz.patch @@ -0,0 +1,42 @@ +# libwebp example binaries require dependencies that aren't available for iOS builds. +# There's also no easy way to invoke the build to *exclude* the example builds. +# Since we don't need the examples anyway, remove them from the Makefile. +# +# As a point of reference, libwebp provides an XCFramework build script that involves +# 7 separate invocations of make to avoid building the examples. Patching the Makefile +# to remove the examples is a simpler approach, and one that is more compatible with +# the existing multibuild infrastructure. +# +# In the next release, it should be possible to pass --disable-libwebpexamples +# instead of applying this patch. +# +diff -ur libwebp-1.5.0-orig/Makefile.am libwebp-1.5.0/Makefile.am +--- libwebp-1.5.0-orig/Makefile.am 2024-12-20 09:17:50 ++++ libwebp-1.5.0/Makefile.am 2025-01-09 11:24:17 +@@ -5,5 +5,3 @@ + if BUILD_EXTRAS + SUBDIRS += extras + endif +- +-SUBDIRS += examples +diff -ur libwebp-1.5.0-orig/Makefile.in libwebp-1.5.0/Makefile.in +--- libwebp-1.5.0-orig/Makefile.in 2024-12-20 09:52:53 ++++ libwebp-1.5.0/Makefile.in 2025-01-09 11:24:17 +@@ -156,7 +156,7 @@ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | $(am__uniquify_input)` +-DIST_SUBDIRS = sharpyuv src imageio man extras examples ++DIST_SUBDIRS = sharpyuv src imageio man extras + am__DIST_COMMON = $(srcdir)/Makefile.in \ + $(top_srcdir)/src/webp/config.h.in AUTHORS COPYING ChangeLog \ + NEWS README.md ar-lib compile config.guess config.sub \ +@@ -351,7 +351,7 @@ + top_srcdir = @top_srcdir@ + webp_libname_prefix = @webp_libname_prefix@ + ACLOCAL_AMFLAGS = -I m4 +-SUBDIRS = sharpyuv src imageio man $(am__append_1) examples ++SUBDIRS = sharpyuv src imageio man $(am__append_1) + EXTRA_DIST = COPYING autogen.sh + all: all-recursive + diff --git a/pyproject.toml b/pyproject.toml index 683ab24ef..abab61e6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ backend-path = [ [project] name = "pillow" -description = "Python Imaging Library (Fork)" +description = "Python Imaging Library (fork)" readme = "README.md" keywords = [ "Imaging", @@ -103,15 +103,55 @@ before-all = ".github/workflows/wheels-dependencies.sh" build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" -# 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" +test-requires = [ + "numpy", +] +xbuild-tools = [ ] + +[tool.cibuildwheel.macos] +# Disable platform guessing on macOS to avoid picking up Homebrew etc. +config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" [tool.cibuildwheel.macos.environment] +# Isolate macOS build environment from Homebrew etc. PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +[tool.cibuildwheel.ios] +# Disable platform guessing on iOS, and disable raqm (since there won't be a +# vendor version, and we can't distribute it due to licensing) +config-settings = "raqm=disable imagequant=disable platform-guessing=disable" + +# iOS needs to be given a specific pytest invocation and list of test sources. +test-sources = [ + "checks", + "Tests", + "selftest.py", +] +test-command = [ + "python -m selftest", + "python -m pytest -vv -x -W always checks/check_wheel.py Tests", +] + +# There's no numpy wheel for iOS (yet...) +test-requires = [ ] + +[[tool.cibuildwheel.overrides]] +# iOS environment is isolated by cibuildwheel, but needs the dependencies +select = "*_iphoneos" +environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" + +[[tool.cibuildwheel.overrides]] +# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies +select = "*_iphonesimulator" +environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" + +[[tool.cibuildwheel.overrides]] +select = "*-win32" +test-requires = [ ] + [tool.black] exclude = "wheels/multibuild" @@ -168,7 +208,7 @@ lint.isort.required-imports = [ max_supported_python = "3.13" [tool.pytest.ini_options] -addopts = "-ra --color=yes" +addopts = "-ra --color=auto" testpaths = [ "Tests", ] diff --git a/setup.py b/setup.py index 354e09f85..477d187a2 100644 --- a/setup.py +++ b/setup.py @@ -473,6 +473,19 @@ class pil_build_ext(build_ext): sdk_path = commandlinetools_sdk_path return sdk_path + def get_ios_sdk_path(self) -> str: + try: + sdk = sys.implementation._multiarch.split("-")[-1] + _dbg("Using %s SDK", sdk) + return ( + subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", sdk]) + .strip() + .decode("latin1") + ) + except Exception: + msg = "Unable to identify location of iOS SDK." + raise ValueError(msg) + def build_extensions(self) -> None: library_dirs: list[str] = [] include_dirs: list[str] = [] @@ -622,6 +635,18 @@ class pil_build_ext(build_ext): for extension in self.extensions: extension.extra_compile_args = ["-Wno-nullability-completeness"] + + elif sys.platform == "ios": + # Add the iOS SDK path. + sdk_path = self.get_ios_sdk_path() + + # Add the iOS SDK path. + _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) + _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include")) + + for extension in self.extensions: + extension.extra_compile_args = ["-Wno-nullability-completeness"] + elif sys.platform.startswith(("linux", "gnu", "freebsd")): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) @@ -877,6 +902,9 @@ class pil_build_ext(build_ext): # so we have to guess; by default it is defined in all Windows builds. # See #4237, #5243, #5359 for more information. defs.append(("USE_WIN32_FILEIO", None)) + elif sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.append("lzma") if feature.get("jpeg"): libs.append(feature.get("jpeg")) defs.append(("HAVE_LIBJPEG", None)) @@ -893,6 +921,9 @@ class pil_build_ext(build_ext): defs.append(("HAVE_LIBIMAGEQUANT", None)) if feature.get("xcb"): libs.append(feature.get("xcb")) + if sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.append("Xau") defs.append(("HAVE_XCB", None)) if sys.platform == "win32": libs.extend(["kernel32", "user32", "gdi32"]) @@ -924,6 +955,11 @@ class pil_build_ext(build_ext): libs.append(feature.get("fribidi")) else: # building FriBiDi shim from src/thirdparty srcs.append("src/thirdparty/fribidi-shim/fribidi.c") + + if sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.extend(["z", "bz2", "brotlicommon", "brotlidec", "png"]) + self._update_extension("PIL._imagingft", libs, defs, srcs) else: @@ -940,6 +976,9 @@ class pil_build_ext(build_ext): webp = feature.get("webp") if isinstance(webp, str): libs = [webp, webp + "mux", webp + "demux"] + if sys.platform == "ios": + # Ensure transitive dependencies are linked. + libs.append("sharpyuv") self._update_extension("PIL._webp", libs) else: self._remove_extension("PIL._webp") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2815cec36..ed726b07d 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -479,8 +479,11 @@ class GifImageFile(ImageFile.ImageFile): self._prev_im = expanded_im assert self._prev_im is not None if self._frame_transparency is not None: - self.im.putpalettealpha(self._frame_transparency, 0) - frame_im = self.im.convert("RGBA") + if self.mode == "L": + frame_im = self.im.convert_transparent("LA", self._frame_transparency) + else: + self.im.putpalettealpha(self._frame_transparency, 0) + frame_im = self.im.convert("RGBA") else: frame_im = self.im.convert("RGB") @@ -489,7 +492,7 @@ class GifImageFile(ImageFile.ImageFile): self.im = self._prev_im self._mode = self.im.mode - if frame_im.mode == "RGBA": + if frame_im.mode in ("LA", "RGBA"): self.im.paste(frame_im, self.dispose_extent, frame_im) else: self.im.paste(frame_im, self.dispose_extent) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 5a88429e5..197ea7a2b 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -25,7 +25,6 @@ import sys from typing import IO from . import Image, ImageFile, PngImagePlugin, features -from ._deprecate import deprecate enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: @@ -275,34 +274,25 @@ class IcnsImageFile(ImageFile.ImageFile): self.best_size[1] * self.best_size[2], ) - @property # type: ignore[override] - def size(self) -> tuple[int, int] | tuple[int, int, int]: + @property + def size(self) -> tuple[int, int]: return self._size @size.setter - def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None: - if len(value) == 3: - deprecate("Setting size to (width, height, scale)", 12, "load(scale)") - if value in self.info["sizes"]: - self._size = value # type: ignore[assignment] + def size(self, value: tuple[int, int]) -> None: + # Check that a matching size exists, + # or that there is a scale that would create a size that matches + for size in self.info["sizes"]: + simple_size = size[0] * size[2], size[1] * size[2] + scale = simple_size[0] // value[0] + if simple_size[1] / value[1] == scale: + self._size = value return - else: - # Check that a matching size exists, - # or that there is a scale that would create a size that matches - for size in self.info["sizes"]: - simple_size = size[0] * size[2], size[1] * size[2] - scale = simple_size[0] // value[0] - if simple_size[1] / value[1] == scale: - self._size = value - return msg = "This is not one of the allowed sizes of this image" raise ValueError(msg) def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: - if scale is not None or len(self.size) == 3: - if scale is None and len(self.size) == 3: - scale = self.size[2] - assert scale is not None + if scale is not None: width, height = self.size[:2] self.size = width * scale, height * scale self.best_size = width, height, scale diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 57d18a821..5ff6a30f5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -115,21 +115,6 @@ except ImportError as v: raise -def isImageType(t: Any) -> TypeGuard[Image]: - """ - Checks if an object is an image object. - - .. warning:: - - This function is for internal use only. - - :param t: object to check if it's an image - :returns: True if the object is an image - """ - deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)") - return hasattr(t, "im") - - # # Constants @@ -219,7 +204,7 @@ if TYPE_CHECKING: from IPython.lib.pretty import PrettyPrinter from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin - from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard + from ._typing import CapsuleType, NumpyArray, StrOrBytesPath ID: list[str] = [] OPEN: dict[ str, @@ -980,9 +965,6 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - self.load() has_transparency = "transparency" in self.info @@ -2229,8 +2211,6 @@ class Image: :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. If the image has mode "1" or "P", it is always set to - :py:data:`Resampling.NEAREST`. If the image mode is "BGR;15", - "BGR;16" or "BGR;24", then the default filter is :py:data:`Resampling.NEAREST`. Otherwise, the default filter is :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. :param box: An optional 4-tuple of floats providing @@ -2253,8 +2233,7 @@ class Image: """ if resample is None: - bgr = self.mode.startswith("BGR;") - resample = Resampling.NEAREST if bgr else Resampling.BICUBIC + resample = Resampling.BICUBIC elif resample not in ( Resampling.NEAREST, Resampling.BILINEAR, @@ -2556,8 +2535,9 @@ class Image: self.load() save_all = params.pop("save_all", None) + self._default_encoderinfo = params encoderinfo = getattr(self, "encoderinfo", {}) - self.encoderinfo = {**encoderinfo, **params} + self._attach_default_encoderinfo(self) self.encoderconfig: tuple[Any, ...] = () if format.upper() not in SAVE: @@ -2599,6 +2579,11 @@ class Image: if open_fp: fp.close() + def _attach_default_encoderinfo(self, im: Image) -> dict[str, Any]: + encoderinfo = getattr(self, "encoderinfo", {}) + self.encoderinfo = {**im._default_encoderinfo, **encoderinfo} + return encoderinfo + def _save_all_progress( self, progress, @@ -3099,9 +3084,6 @@ def new( :returns: An :py:class:`~PIL.Image.Image` object. """ - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) - _check_size(size) if color is None: @@ -3524,8 +3506,6 @@ def open( filename: str | bytes = "" if is_path(fp): filename = os.fspath(fp) - - if filename: fp = builtins.open(filename, "rb") exclusive_fp = True else: diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index a1584f111..d3555694a 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -23,10 +23,9 @@ import operator import sys from enum import IntEnum, IntFlag from functools import reduce -from typing import Any, Literal, SupportsFloat, SupportsInt, Union +from typing import Literal, SupportsFloat, SupportsInt, Union -from . import Image, __version__ -from ._deprecate import deprecate +from . import Image from ._typing import SupportsRead try: @@ -108,20 +107,6 @@ pyCMS _VERSION = "1.0.0 pil" -def __getattr__(name: str) -> Any: - if name == "DESCRIPTION": - deprecate("PIL.ImageCms.DESCRIPTION", 12) - return _DESCRIPTION - elif name == "VERSION": - deprecate("PIL.ImageCms.VERSION", 12) - return _VERSION - elif name == "FLAGS": - deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") - return _FLAGS - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # --------------------------------------------------------------------. @@ -301,31 +286,6 @@ class ImageCmsTransform(Image.ImagePointHandler): proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, flags: Flags = Flags.NONE, ): - supported_modes = ( - "RGB", - "RGBA", - "RGBX", - "CMYK", - "I;16", - "I;16L", - "I;16B", - "YCbCr", - "LAB", - "L", - "1", - ) - for mode in (input_mode, output_mode): - if mode not in supported_modes: - deprecate( - mode, - 12, - { - "L;16": "I;16 or I;16L", - "L:16B": "I;16B", - "YCCA": "YCbCr", - "YCC": "YCbCr", - }.get(mode), - ) if proof is None: self.transform = core.buildTransform( input.profile, output.profile, input_mode, output_mode, intent, flags @@ -1108,16 +1068,3 @@ def isIntentSupported( return -1 except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) from v - - -def versions() -> tuple[str, str | None, str, str]: - """ - (pyCMS) Fetches versions. - """ - - deprecate( - "PIL.ImageCms.versions()", - 12, - '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', - ) - return _VERSION, core.littlecms_version, sys.version.split()[0], __version__ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 6cf1ee626..e95fa91f8 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -38,7 +38,6 @@ from types import ModuleType from typing import Any, AnyStr, Callable, Union, cast from . import Image, ImageColor -from ._deprecate import deprecate from ._typing import Coords # experimental access to the outline API @@ -1009,16 +1008,11 @@ def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: return ImageDraw(im, mode) -def getdraw( - im: Image.Image | None = None, hints: list[str] | None = None -) -> tuple[ImageDraw2.Draw | None, ModuleType]: +def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]: """ :param im: The image to draw in. - :param hints: An optional list of hints. Deprecated. :returns: A (drawing context, drawing resource factory) tuple. """ - if hints is not None: - deprecate("'hints' parameter", 12) from . import ImageDraw2 draw = ImageDraw2.Draw(im) if im is not None else None diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index bf556a2c6..27b27127e 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -37,7 +37,6 @@ import struct from typing import IO, Any, NamedTuple, cast from . import ExifTags, Image -from ._deprecate import deprecate from ._util import DeferredError, is_path TYPE_CHECKING = False @@ -83,16 +82,6 @@ def _get_oserror(error: int, *, encoder: bool) -> OSError: return OSError(msg) -def raise_oserror(error: int) -> OSError: - deprecate( - "raise_oserror", - 12, - action="It is only useful for translating error codes returned by a codec's " - "decode() method, which ImageFile already does automatically.", - ) - raise _get_oserror(error, encoder=False) - - def _tilesort(t: _Tile) -> int: # sort on offset return t[2] diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 329c463ff..bf3f471f5 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -36,7 +36,7 @@ from io import BytesIO from types import ModuleType from typing import IO, Any, BinaryIO, TypedDict, cast -from . import Image, features +from . import Image from ._typing import StrOrBytesPath from ._util import DeferredError, is_path @@ -236,21 +236,6 @@ class FreeTypeFont: self.index = index self.encoding = encoding - try: - from packaging.version import parse as parse_version - except ImportError: - pass - else: - if freetype_version := features.version_module("freetype2"): - if parse_version(freetype_version) < parse_version("2.9.1"): - warnings.warn( - "Support for FreeType 2.9.0 is deprecated and will be removed " - "in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 " - "or newer, preferably FreeType 2.10.4 which fixes " - "CVE-2020-15999.", - DeprecationWarning, - ) - if layout_engine not in (Layout.BASIC, Layout.RAQM): layout_engine = Layout.BASIC if core.HAVE_RAQM: diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index c33809ced..d2504b1ae 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -21,7 +21,6 @@ from types import CodeType from typing import Any, Callable from . import Image, _imagingmath -from ._deprecate import deprecate class _Operand: @@ -233,11 +232,7 @@ ops = { } -def lambda_eval( - expression: Callable[[dict[str, Any]], Any], - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: +def lambda_eval(expression: Callable[[dict[str, Any]], Any], **kw: Any) -> Any: """ Returns the result of an image function. @@ -246,23 +241,13 @@ def lambda_eval( :py:func:`~PIL.Image.merge` function. :param expression: A function that receives a dictionary. - :param options: Values to add to the function's dictionary. Deprecated. - You can instead use one or more keyword arguments. :param **kw: Values to add to the function's dictionary. :return: The expression result. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. """ - if options: - deprecate( - "ImageMath.lambda_eval options", - 12, - "ImageMath.lambda_eval keyword arguments", - ) - args: dict[str, Any] = ops.copy() - args.update(options) args.update(kw) for k, v in args.items(): if isinstance(v, Image.Image): @@ -275,11 +260,7 @@ def lambda_eval( return out -def unsafe_eval( - expression: str, - options: dict[str, Any] = {}, - **kw: Any, -) -> Any: +def unsafe_eval(expression: str, **kw: Any) -> Any: """ Evaluates an image expression. This uses Python's ``eval()`` function to process the expression string, and carries the security risks of doing so. It is not @@ -291,29 +272,19 @@ def unsafe_eval( :py:func:`~PIL.Image.merge` function. :param expression: A string containing a Python-style expression. - :param options: Values to add to the evaluation context. Deprecated. - You can instead use one or more keyword arguments. :param **kw: Values to add to the evaluation context. :return: The evaluated expression. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. """ - if options: - deprecate( - "ImageMath.unsafe_eval options", - 12, - "ImageMath.unsafe_eval keyword arguments", - ) - # build execution namespace args: dict[str, Any] = ops.copy() - for k in [*options, *kw]: + for k in kw: if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" raise ValueError(msg) - args.update(options) args.update(kw) for k, v in args.items(): if isinstance(v, Image.Image): @@ -337,32 +308,3 @@ def unsafe_eval( return out.im except AttributeError: return out - - -def eval( - expression: str, - _dict: dict[str, Any] = {}, - **kw: Any, -) -> Any: - """ - Evaluates an image expression. - - Deprecated. Use lambda_eval() or unsafe_eval() instead. - - :param expression: A string containing a Python-style expression. - :param _dict: Values to add to the evaluation context. You - can either use a dictionary, or one or more keyword - arguments. - :return: The evaluated expression. This is usually an image object, but can - also be an integer, a floating point value, or a pixel tuple, - depending on the expression. - - .. deprecated:: 10.3.0 - """ - - deprecate( - "ImageMath.eval", - 12, - "ImageMath.lambda_eval or ImageMath.unsafe_eval", - ) - return unsafe_eval(expression, _dict, **kw) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 92a08d2cb..b7c6c8636 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -18,8 +18,6 @@ import sys from functools import lru_cache from typing import NamedTuple -from ._deprecate import deprecate - class ModeDescriptor(NamedTuple): """Wrapper for mode strings.""" @@ -57,16 +55,11 @@ def getmode(mode: str) -> ModeDescriptor: "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), # extra experimental modes "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), "LA": ("L", "L", ("L", "A"), "|u1"), "La": ("L", "L", ("L", "a"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"), } if mode in modes: - if mode in ("BGR;15", "BGR;16", "BGR;24"): - deprecate(mode, 12) base_mode, base_type, bands, type_str = modes[mode] return ModeDescriptor(mode, bands, base_mode, base_type, type_str) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 637f67810..b1fbb1bf1 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -16,26 +16,16 @@ # from __future__ import annotations -from collections.abc import Sequence from io import BytesIO from typing import cast from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import i32be as i32 -from ._deprecate import deprecate COMPRESSION = {1: "raw", 5: "jpeg"} -def __getattr__(name: str) -> bytes: - if name == "PAD": - deprecate("IptcImagePlugin.PAD", 12) - return b"\0\0\0\0" - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # # Helpers @@ -48,20 +38,6 @@ def _i8(c: int | bytes) -> int: return c if isinstance(c, int) else c[0] -def i(c: bytes) -> int: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.i", 12) - return _i(c) - - -def dump(c: Sequence[int | bytes]) -> None: - """.. deprecated:: 10.2.0""" - deprecate("IptcImagePlugin.dump", 12) - for i in c: - print(f"{_i8(i):02x}", end=" ") - print() - - ## # Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields # from TIFF and JPEG files, use the getiptcinfo function. @@ -220,7 +196,7 @@ def getiptcinfo( # get raw data from the IPTC/NAA tag (PhotoShop tags the data # as 4-byte integers, so we cannot use the get method...) try: - data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK] + data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] except KeyError: pass diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index defe9f773..082f3551a 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -49,7 +49,6 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 -from ._deprecate import deprecate from .JpegPresets import presets TYPE_CHECKING = False @@ -393,12 +392,6 @@ class JpegImageFile(ImageFile.ImageFile): self._read_dpi_from_exif() - def __getattr__(self, name: str) -> Any: - if name in ("huffman_ac", "huffman_dc"): - deprecate(name, 12) - return getattr(self, "_" + name) - raise AttributeError(name) - def __getstate__(self) -> list[Any]: return super().__getstate__() + [self.layers, self.layer] diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 4c34dd7e5..9a47933b6 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -50,9 +50,7 @@ class McIdasImageFile(ImageFile.ImageFile): if w[11] == 1: mode = rawmode = "L" elif w[11] == 2: - # FIXME: add memory map support - mode = "I" - rawmode = "I;16B" + mode = rawmode = "I;16B" elif w[11] == 4: # FIXME: add memory map support mode = "I" diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index f4570212f..80a4ba67f 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -73,7 +73,9 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) else: + encoderinfo = im_frame._attach_default_encoderinfo(im) im_frame.save(fp, "JPEG") + im_frame.encoderinfo = encoderinfo offsets.append(fp.tell() - offsets[-1]) if progress: completed += 1 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f2e70e469..07ddece6b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -56,7 +56,6 @@ from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 -from ._deprecate import deprecate from ._typing import StrOrBytesPath from ._util import DeferredError, is_path from .TiffTags import TYPES @@ -284,9 +283,6 @@ PREFIXES = [ b"II\x2b\x00", # BigTIFF with little-endian byte order ] -if not getattr(Image.core, "libtiff_support_custom_tags", True): - deprecate("Support for LibTIFF earlier than version 4", 12) - def _accept(prefix: bytes) -> bool: return prefix.startswith(tuple(PREFIXES)) @@ -1934,9 +1930,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - if not getattr(Image.core, "libtiff_support_custom_tags", False): - continue - if tag in TiffTags.TAGS_V2_GROUPS: types[tag] = TiffTags.LONG8 elif tag in ifd.tagtype: @@ -2318,8 +2311,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: try: with AppendingTiffWriter(fp) as tf: for i, seq in enumerate(im_sequences): - if not hasattr(seq, "encoderinfo"): - seq.encoderinfo = {} + encoderinfo = seq._attach_default_encoderinfo(im) if not hasattr(seq, "encoderconfig"): seq.encoderconfig = () nfr = getattr(seq, "n_frames", 1) @@ -2333,6 +2325,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: im._save_all_progress(progress, seq, i, completed, total) tf.newFrame() + seq.encoderinfo = encoderinfo finally: im.seek(cur_idx) diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 170d44490..616a9aace 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -46,8 +46,6 @@ def deprecate( elif when <= int(__version__.split(".")[0]): msg = f"{deprecated} {is_} deprecated and should be removed." raise RuntimeError(msg) - elif when == 12: - removed = "Pillow 12 (2025-10-15)" elif when == 13: removed = "Pillow 13 (2026-10-15)" else: diff --git a/src/PIL/_version.py b/src/PIL/_version.py index ac678c7d2..6a3c01f26 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.3.0.dev0" +__version__ = "12.0.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 573f1d412..984f7532c 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -121,9 +121,6 @@ def get_supported_codecs() -> list[str]: features: dict[str, tuple[str, str | bool, str | None]] = { - "webp_anim": ("PIL._webp", True, None), - "webp_mux": ("PIL._webp", True, None), - "transp_webp": ("PIL._webp", True, None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), diff --git a/src/_imaging.c b/src/_imaging.c index 6f13834a9..fbfc0e41a 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -681,30 +681,6 @@ getink(PyObject *color, Imaging im, char *ink) { } else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) { return NULL; } - if (!strcmp(im->mode, "BGR;15")) { - UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + - ((((UINT16)g) << 2) & 0x03e0) + - ((((UINT16)b) >> 3) & 0x001f); - - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } else if (!strcmp(im->mode, "BGR;16")) { - UINT16 v = ((((UINT16)r) << 8) & 0xf800) + - ((((UINT16)g) << 3) & 0x07e0) + - ((((UINT16)b) >> 3) & 0x001f); - ink[0] = (UINT8)v; - ink[1] = (UINT8)(v >> 8); - ink[2] = ink[3] = 0; - return ink; - } else if (!strcmp(im->mode, "BGR;24")) { - ink[0] = (UINT8)b; - ink[1] = (UINT8)g; - ink[2] = (UINT8)r; - ink[3] = 0; - return ink; - } } } @@ -1650,54 +1626,33 @@ _putdata(ImagingObject *self, PyObject *args) { return NULL; } double value; - if (image->bands == 1) { - int bigendian = 0; - if (image->type == IMAGING_TYPE_SPECIAL) { - // I;16* - if ( - strcmp(image->mode, "I;16B") == 0 + int bigendian = 0; + if (image->type == IMAGING_TYPE_SPECIAL) { + // I;16* + if ( + strcmp(image->mode, "I;16B") == 0 #ifdef WORDS_BIGENDIAN - || strcmp(image->mode, "I;16N") == 0 + || strcmp(image->mode, "I;16N") == 0 #endif - ) { - bigendian = 1; - } + ) { + bigendian = 1; } - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - if (scale != 1.0 || offset != 0.0) { - value = value * scale + offset; - } - if (image->type == IMAGING_TYPE_SPECIAL) { - image->image8[y][x * 2 + (bigendian ? 1 : 0)] = - CLIP8((int)value % 256); - image->image8[y][x * 2 + (bigendian ? 0 : 1)] = - CLIP8((int)value >> 8); - } else { - image->image8[y][x] = (UINT8)CLIP8(value); - } - if (++x >= (int)image->xsize) { - x = 0, y++; - } + } + for (i = x = y = 0; i < n; i++) { + set_value_to_item(seq, i); + if (scale != 1.0 || offset != 0.0) { + value = value * scale + offset; } - } else { - // BGR;* - int b; - for (i = x = y = 0; i < n; i++) { - char ink[4]; - - op = PySequence_Fast_GET_ITEM(seq, i); - if (!op || !getink(op, image, ink)) { - Py_DECREF(seq); - return NULL; - } - /* FIXME: what about scale and offset? */ - for (b = 0; b < image->pixelsize; b++) { - image->image8[y][x * image->pixelsize + b] = ink[b]; - } - if (++x >= (int)image->xsize) { - x = 0, y++; - } + if (image->type == IMAGING_TYPE_SPECIAL) { + image->image8[y][x * 2 + (bigendian ? 1 : 0)] = + CLIP8((int)value % 256); + image->image8[y][x * 2 + (bigendian ? 0 : 1)] = + CLIP8((int)value >> 8); + } else { + image->image8[y][x] = (UINT8)CLIP8(value); + } + if (++x >= (int)image->xsize) { + x = 0, y++; } } PyErr_Clear(); /* Avoid weird exceptions */ @@ -3769,18 +3724,6 @@ _getattr_bands(ImagingObject *self, void *closure) { return PyLong_FromLong(self->image->bands); } -static PyObject * -_getattr_id(ImagingObject *self, void *closure) { - if (PyErr_WarnEx( - PyExc_DeprecationWarning, - "id property is deprecated and will be removed in Pillow 12 (2025-10-15)", - 1 - ) < 0) { - return NULL; - } - return PyLong_FromSsize_t((Py_ssize_t)self->image); -} - static void _ptr_destructor(PyObject *capsule) { PyObject *self = (PyObject *)PyCapsule_GetContext(capsule); @@ -3795,27 +3738,6 @@ _getattr_ptr(ImagingObject *self, void *closure) { return capsule; } -static PyObject * -_getattr_unsafe_ptrs(ImagingObject *self, void *closure) { - if (PyErr_WarnEx( - PyExc_DeprecationWarning, - "unsafe_ptrs property is deprecated and will be removed in Pillow 12 " - "(2025-10-15)", - 1 - ) < 0) { - return NULL; - } - return Py_BuildValue( - "(sn)(sn)(sn)", - "image8", - self->image->image8, - "image32", - self->image->image32, - "image", - self->image->image - ); -} - static PyObject * _getattr_readonly(ImagingObject *self, void *closure) { return PyLong_FromLong(self->image->read_only); @@ -3825,9 +3747,7 @@ static struct PyGetSetDef getsetters[] = { {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, {"bands", (getter)_getattr_bands}, - {"id", (getter)_getattr_id}, {"ptr", (getter)_getattr_ptr}, - {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, {"readonly", (getter)_getattr_readonly}, {NULL} }; @@ -4432,16 +4352,6 @@ setup_module(PyObject *m) { PyObject *v = PyUnicode_FromString(ImagingTiffVersion()); PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None); Py_XDECREF(v); - - // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 - PyObject *support_custom_tags; -#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ - TIFFLIB_VERSION != 20120922 - support_custom_tags = Py_True; -#else - support_custom_tags = Py_False; -#endif - PyDict_SetItemString(d, "libtiff_support_custom_tags", support_custom_tags); } #endif diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 1c1937105..3db52377e 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -82,31 +82,6 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } -static void -get_pixel_BGR15(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; - UINT16 pixel = in[0] + (in[1] << 8); - char *out = color; - out[0] = (pixel & 31) * 255 / 31; - out[1] = ((pixel >> 5) & 31) * 255 / 31; - out[2] = ((pixel >> 10) & 31) * 255 / 31; -} - -static void -get_pixel_BGR16(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; - UINT16 pixel = in[0] + (in[1] << 8); - char *out = color; - out[0] = (pixel & 31) * 255 / 31; - out[1] = ((pixel >> 5) & 63) * 255 / 63; - out[2] = ((pixel >> 11) & 31) * 255 / 31; -} - -static void -get_pixel_BGR24(Imaging im, int x, int y, void *color) { - memcpy(color, &im->image8[y][x * 3], sizeof(UINT8) * 3); -} - static void get_pixel_32(Imaging im, int x, int y, void *color) { memcpy(color, &im->image32[y][x], sizeof(INT32)); @@ -154,16 +129,6 @@ put_pixel_16B(Imaging im, int x, int y, const void *color) { out[1] = in[0]; } -static void -put_pixel_BGR1516(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 2], color, 2); -} - -static void -put_pixel_BGR24(Imaging im, int x, int y, const void *color) { - memcpy(&im->image8[y][x * 3], color, 3); -} - static void put_pixel_32L(Imaging im, int x, int y, const void *color) { memcpy(&im->image8[y][x * 4], color, 4); @@ -212,9 +177,6 @@ ImagingAccessInit(void) { ADD("F", get_pixel_32, put_pixel_32); ADD("P", get_pixel_8, put_pixel_8); ADD("PA", get_pixel_32_2bands, put_pixel_32); - ADD("BGR;15", get_pixel_BGR15, put_pixel_BGR1516); - ADD("BGR;16", get_pixel_BGR16, put_pixel_BGR1516); - ADD("BGR;24", get_pixel_BGR24, put_pixel_BGR24); ADD("RGB", get_pixel_32, put_pixel_32); ADD("RGBA", get_pixel_32, put_pixel_32); ADD("RGBa", get_pixel_32, put_pixel_32); diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index c8f234261..9a2c9ff16 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -277,38 +277,6 @@ rgb2f(UINT8 *out_, const UINT8 *in, int xsize) { } } -static void -rgb2bgr15(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out_ += 2) { - UINT16 v = ((((UINT16)in[0]) << 7) & 0x7c00) + - ((((UINT16)in[1]) << 2) & 0x03e0) + - ((((UINT16)in[2]) >> 3) & 0x001f); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -rgb2bgr16(UINT8 *out_, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4, out_ += 2) { - UINT16 v = ((((UINT16)in[0]) << 8) & 0xf800) + - ((((UINT16)in[1]) << 3) & 0x07e0) + - ((((UINT16)in[2]) >> 3) & 0x001f); - memcpy(out_, &v, sizeof(v)); - } -} - -static void -rgb2bgr24(UINT8 *out, const UINT8 *in, int xsize) { - int x; - for (x = 0; x < xsize; x++, in += 4) { - *out++ = in[2]; - *out++ = in[1]; - *out++ = in[0]; - } -} - static void rgb2hsv_row(UINT8 *out, const UINT8 *in) { // following colorsys.py float h, s, rc, gc, bc, cr; @@ -971,9 +939,6 @@ static struct { {"RGB", "I;16N", rgb2i16l}, #endif {"RGB", "F", rgb2f}, - {"RGB", "BGR;15", rgb2bgr15}, - {"RGB", "BGR;16", rgb2bgr16}, - {"RGB", "BGR;24", rgb2bgr24}, {"RGB", "RGBA", rgb2rgba}, {"RGB", "RGBa", rgb2rgba}, {"RGB", "RGBX", rgb2rgba}, diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 79a38e12f..972435ee1 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -175,18 +175,21 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Use custom quantization tables */ if (context->qtables) { int i; - int quality = 100; + int quality = 50; int last_q = 0; + boolean force_baseline = FALSE; if (context->quality != -1) { quality = context->quality; + force_baseline = TRUE; } + int scale_factor = jpeg_quality_scaling(quality); for (i = 0; i < context->qtablesLen; i++) { jpeg_add_quant_table( &context->cinfo, i, &context->qtables[i * DCTSIZE2], - quality, - FALSE + scale_factor, + force_baseline ); context->cinfo.comp_info[i].quant_tbl_no = i; last_q = i; @@ -195,7 +198,11 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { // jpeg_set_defaults created two qtables internally, but we only // wanted one. jpeg_add_quant_table( - &context->cinfo, 1, &context->qtables[0], quality, FALSE + &context->cinfo, + 1, + &context->qtables[0], + scale_factor, + force_baseline ); } for (i = last_q; i < context->cinfo.num_components; i++) { diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index c29473d90..7f8a50d19 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -471,12 +471,6 @@ copy2(UINT8 *out, const UINT8 *in, int pixels) { memcpy(out, in, pixels * 2); } -static void -copy3(UINT8 *out, const UINT8 *in, int pixels) { - /* BGR;24, etc */ - memcpy(out, in, pixels * 3); -} - static void copy4(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples */ @@ -657,9 +651,6 @@ static struct { {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, packI16N_I16}, {"I;16B", "I;16N", 16, packI16N_I16B}, - {"BGR;15", "BGR;15", 16, copy2}, - {"BGR;16", "BGR;16", 16, copy2}, - {"BGR;24", "BGR;24", 24, copy3}, {NULL} /* sentinel */ }; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 6fe26e1bd..4640f078a 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -151,36 +151,6 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { strcpy(im->band_names[2], "B"); strcpy(im->band_names[3], "X"); - } else if (strcmp(mode, "BGR;15") == 0) { - /* EXPERIMENTAL */ - /* 15-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - /* not allowing arrow due to line length packing */ - strcpy(im->arrow_band_format, ""); - - } else if (strcmp(mode, "BGR;16") == 0) { - /* EXPERIMENTAL */ - /* 16-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - /* not allowing arrow due to line length packing */ - strcpy(im->arrow_band_format, ""); - - } else if (strcmp(mode, "BGR;24") == 0) { - /* EXPERIMENTAL */ - /* 24-bit reversed true colour */ - im->bands = 3; - im->pixelsize = 3; - im->linesize = (xsize * 3 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - /* not allowing arrow due to line length packing */ - strcpy(im->arrow_band_format, ""); - } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index e289ce405..71516fd1b 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -884,7 +884,6 @@ ImagingLibTiffMergeFieldInfo( // Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html) TIFFSTATE *clientstate = (TIFFSTATE *)state->context; uint32_t n; - int status = 0; // custom fields added with ImagingLibTiffMergeFieldInfo are only used for // decoding, ignore readcount; @@ -907,14 +906,7 @@ ImagingLibTiffMergeFieldInfo( n = sizeof(info) / sizeof(info[0]); - // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 -#if TIFFLIB_VERSION >= 20111221 && TIFFLIB_VERSION != 20120218 && \ - TIFFLIB_VERSION != 20120922 - status = TIFFMergeFieldInfo(clientstate->tiff, info, n); -#else - TIFFMergeFieldInfo(clientstate->tiff, info, n); -#endif - return status; + return TIFFMergeFieldInfo(clientstate->tiff, info, n); } int diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 9c3ee2665..976baa726 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1284,12 +1284,6 @@ copy2(UINT8 *out, const UINT8 *in, int pixels) { memcpy(out, in, pixels * 2); } -static void -copy3(UINT8 *out, const UINT8 *in, int pixels) { - /* BGR;24 */ - memcpy(out, in, pixels * 3); -} - static void copy4(UINT8 *out, const UINT8 *in, int pixels) { /* RGBA, CMYK quadruples */ @@ -1649,10 +1643,6 @@ static struct { {"RGB", "B;16B", 16, band216B}, {"RGB", "CMYK", 32, cmyk2rgb}, - {"BGR;15", "BGR;15", 16, copy2}, - {"BGR;16", "BGR;16", 16, copy2}, - {"BGR;24", "BGR;24", 24, copy3}, - /* true colour w. alpha */ {"RGBA", "LA", 16, unpackRGBALA}, {"RGBA", "LA;16B", 32, unpackRGBALA16B},