Merge branch 'main' into imagecms-typing

This commit is contained in:
Andrew Murray 2025-07-08 21:19:04 +10:00
commit f6df21baee
139 changed files with 13517 additions and 1386 deletions

View File

@ -1 +1 @@
cibuildwheel==3.0.0 cibuildwheel==3.0.1

View File

@ -1,3 +1,3 @@
python.exe -c "from PIL import Image" python.exe -c "from PIL import Image"
IF ERRORLEVEL 1 EXIT /B IF ERRORLEVEL 1 EXIT /B
python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests python.exe -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests

View File

@ -4,4 +4,4 @@ set -e
python3 -c "from PIL import Image" python3 -c "from PIL import Image"
python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE python3 -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE

View File

@ -18,6 +18,5 @@ exclude_also =
[run] [run]
omit = omit =
Tests/32bit_segfault_check.py checks/*.py
Tests/check_*.py
Tests/createfontdatachunk.py Tests/createfontdatachunk.py

View File

@ -1,42 +1,98 @@
#!/bin/bash #!/bin/bash
# Setup that needs to be done before multibuild utils are invoked # Safety check - Pillow builds require that CIBW_ARCHS is set, and that it only
PROJECTDIR=$(pwd) # contains a single value (even though cibuildwheel allows multiple values in
if [[ "$(uname -s)" == "Darwin" ]]; then # CIBW_ARCHS). This check doesn't work on Linux because of how the CIBW_ARCHS
# Safety check - macOS builds require that CIBW_ARCHS is set, and that it # variable is exposed.
# only contains a single value (even though cibuildwheel allows multiple function check_cibw_archs {
# values in CIBW_ARCHS).
if [[ -z "$CIBW_ARCHS" ]]; then 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 exit 1
fi fi
if [[ "$CIBW_ARCHS" == *" "* ]]; then 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 exit 1
fi 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` # Build macOS dependencies in `build/darwin`
# Install them into `build/deps/darwin` # Install them into `build/deps/darwin`
PLAT=$CIBW_ARCHS
WORKDIR=$(pwd)/build/darwin WORKDIR=$(pwd)/build/darwin
BUILD_PREFIX=$(pwd)/build/deps/darwin BUILD_PREFIX=$(pwd)/build/deps/darwin
else else
# Build prefix will default to /usr/local # Build prefix will default to /usr/local
PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
WORKDIR=$(pwd)/build WORKDIR=$(pwd)/build
MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
MB_ML_VER=${AUDITWHEEL_POLICY:9} MB_ML_VER=${AUDITWHEEL_POLICY:9}
fi fi
PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
# Define custom utilities # Define custom utilities
source wheels/multibuild/common_utils.sh source wheels/multibuild/common_utils.sh
source wheels/multibuild/library_builders.sh source wheels/multibuild/library_builders.sh
if [ -z "$IS_MACOS" ]; then if [[ -z "$IS_MACOS" ]]; then
source wheels/multibuild/manylinux_utils.sh source wheels/multibuild/manylinux_utils.sh
fi fi
ARCHIVE_SDIR=pillow-depends-main 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 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.2.1 HARFBUZZ_VERSION=11.2.1
LIBPNG_VERSION=1.6.49 LIBPNG_VERSION=1.6.49
@ -47,31 +103,58 @@ TIFF_VERSION=4.7.0
LCMS2_VERSION=2.17 LCMS2_VERSION=2.17
ZLIB_VERSION=1.3.1 ZLIB_VERSION=1.3.1
ZLIB_NG_VERSION=2.2.4 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 BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0 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 { function build_pkg_config {
if [ -e pkg-config-stamp ]; then return; fi if [ -e pkg-config-stamp ]; then return; fi
# This essentially duplicates the Homebrew recipe # 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 \ # 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 \ --disable-debug --disable-host-tool --with-internal-glib \
--with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \
--with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include --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 export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config
touch pkg-config-stamp touch pkg-config-stamp
} }
function build_zlib_ng { function build_zlib_ng {
if [ -e zlib-stamp ]; then return; fi 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 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 # 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 # @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 # 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 install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
fi fi
touch zlib-stamp touch zlib-stamp
@ -81,7 +164,7 @@ function build_brotli {
if [ -e brotli-stamp ]; then return; fi 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) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \ (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) && make install)
touch brotli-stamp touch brotli-stamp
} }
@ -92,12 +175,65 @@ 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) 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 \ (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 \ (cd $out_dir/build \
&& meson install) && meson install)
touch harfbuzz-stamp touch harfbuzz-stamp
} }
function build_libavif {
if [ -e libavif-stamp ]; then return; fi
python3 -m pip install meson ninja
if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
fi
local build_type=MinSizeRel
local lto=ON
local libavif_cmake_flags
if [ -n "$IS_MACOS" ]; then
lto=OFF
libavif_cmake_flags=(
-DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \
)
else
if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then
build_type=Release
fi
libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now")
fi
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
# CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject
# of libavif) that disables support for encoding high bit depth images.
(cd $out_dir \
&& cmake \
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
-DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \
-DBUILD_SHARED_LIBS=ON \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_AOM=LOCAL \
-DCONFIG_AV1_HIGHBITDEPTH=0 \
-DAVIF_CODEC_AOM_DECODE=OFF \
-DAVIF_CODEC_DAV1D=LOCAL \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
-DCMAKE_C_VISIBILITY_PRESET=hidden \
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \
-DCMAKE_BUILD_TYPE=$build_type \
"${libavif_cmake_flags[@]}" \
. \
&& make install)
touch libavif-stamp
}
function build { function build {
build_xz build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
@ -110,19 +246,19 @@ function build {
fi fi
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto 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 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 libXau 1.0.12 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
else 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 fi
build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
build_libjpeg_turbo 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 # 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 # libdeflate and zstd, because on x86_64 macs, it will pick up the
# Homebrew versions of those libraries from /usr/local. # Homebrew versions of those libraries from /usr/local.
build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \
@ -132,6 +268,10 @@ function build {
build_tiff build_tiff
fi fi
if [[ -z "$IOS_SDK" ]]; then
# Short term workaround; don't build libavif on iOS
build_libavif
fi
build_libpng build_libpng
build_lcms2 build_lcms2
build_openjpeg build_openjpeg
@ -146,14 +286,44 @@ function build {
build_brotli build_brotli
if [ -n "$IS_MACOS" ]; then if [[ -n "$IS_MACOS" ]]; then
# Custom freetype build # Custom freetype build
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
else else
build_freetype build_freetype
fi 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. # Perform all dependency builds in the build subfolder.
@ -172,24 +342,40 @@ if [[ ! -d $WORKDIR/pillow-depends-main ]]; then
fi fi
if [[ -n "$IS_MACOS" ]]; then 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. # Ensure the basic structure of the build prefix directory exists.
mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/bin"
mkdir -p "$BUILD_PREFIX/lib" 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 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 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 fi
wrap_wheel_builder build wrap_wheel_builder build

View File

@ -15,15 +15,12 @@ if (Test-Path $venv\Scripts\pypy.exe) {
$python = "python.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 & 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 cd $pillow
& $venv\Scripts\$python -VV & $venv\Scripts\$python -VV
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python selftest.py & $venv\Scripts\$python selftest.py
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py & $venv\Scripts\$python -m pytest -vv -x checks\check_wheel.py
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python -m pytest -vx Tests & $venv\Scripts\$python -m pytest -vv -x Tests
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }

View File

@ -25,8 +25,6 @@ else
yum install -y fribidi yum install -y fribidi
fi fi
python3 -m pip install numpy
if [ ! -d "test-images-main" ]; then if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
unzip pillow-test-images.zip unzip pillow-test-images.zip
@ -35,5 +33,5 @@ fi
# Runs tests # Runs tests
python3 selftest.py python3 selftest.py
python3 -m pytest Tests/check_wheel.py python3 -m pytest -vv -x checks/check_wheel.py
python3 -m pytest python3 -m pytest -vv -x

View File

@ -51,40 +51,60 @@ jobs:
matrix: matrix:
include: include:
- name: "macOS 10.10 x86_64" - name: "macOS 10.10 x86_64"
platform: macos
os: macos-13 os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
build: "cp3{9,10,11}*" build: "cp3{9,10,11}*"
macosx_deployment_target: "10.10" macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64" - name: "macOS 10.13 x86_64"
platform: macos
os: macos-13 os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
build: "cp3{12,13,14}*" build: "cp3{12,13,14}*"
macosx_deployment_target: "10.13" macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64" - name: "macOS 10.15 x86_64"
platform: macos
os: macos-13 os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
build: "pp3*" build: "pp3*"
macosx_deployment_target: "10.15" macosx_deployment_target: "10.15"
- name: "macOS arm64" - name: "macOS arm64"
platform: macos
os: macos-latest os: macos-latest
cibw_arch: arm64 cibw_arch: arm64
macosx_deployment_target: "11.0" macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64" - name: "manylinux2014 and musllinux x86_64"
platform: linux
os: ubuntu-latest os: ubuntu-latest
cibw_arch: x86_64 cibw_arch: x86_64
manylinux: "manylinux2014"
- name: "manylinux_2_28 x86_64" - name: "manylinux_2_28 x86_64"
platform: linux
os: ubuntu-latest os: ubuntu-latest
cibw_arch: x86_64 cibw_arch: x86_64
build: "*manylinux*" build: "*manylinux*"
manylinux: "manylinux_2_28"
- name: "manylinux2014 and musllinux aarch64" - name: "manylinux2014 and musllinux aarch64"
platform: linux
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
cibw_arch: aarch64 cibw_arch: aarch64
manylinux: "manylinux2014"
- name: "manylinux_2_28 aarch64" - name: "manylinux_2_28 aarch64"
platform: linux
os: ubuntu-24.04-arm os: ubuntu-24.04-arm
cibw_arch: aarch64 cibw_arch: aarch64
build: "*manylinux*" 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -103,6 +123,7 @@ jobs:
run: | run: |
python3 -m cibuildwheel --output-dir wheelhouse python3 -m cibuildwheel --output-dir wheelhouse
env: env:
CIBW_PLATFORM: ${{ matrix.platform }}
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }} CIBW_BUILD: ${{ matrix.build }}
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
@ -114,7 +135,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: 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 path: ./wheelhouse/*.whl
windows: windows:
@ -159,7 +180,7 @@ jobs:
# Install extra test images # Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images xcopy /S /Y Tests\test-images\* Tests\images
& python.exe winbuild\build_prepare.py -v --no-imagequant --no-avif --architecture=${{ matrix.cibw_arch }} & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
shell: pwsh shell: pwsh
- name: Build wheels - name: Build wheels

View File

@ -1,8 +1,8 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.12 rev: v0.12.2
hooks: hooks:
- id: ruff - id: ruff-check
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
@ -11,7 +11,7 @@ repos:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.8.3 rev: 1.8.6
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -21,10 +21,10 @@ repos:
rev: v1.5.5 rev: v1.5.5
hooks: hooks:
- id: remove-tabs - 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 - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.5 rev: v20.1.7
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -46,19 +46,19 @@ repos:
- id: check-yaml - id: check-yaml
args: [--allow-multiple-documents] args: [--allow-multiple-documents]
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: ^Tests/images/ exclude: ^Tests/images/|\.patch$
- id: trailing-whitespace - id: trailing-whitespace
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.0 rev: 0.33.2
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
- id: check-renovate - id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit - repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.9.0 rev: v1.11.0
hooks: hooks:
- id: zizmor - id: zizmor

View File

@ -13,6 +13,8 @@ include LICENSE
include Makefile include Makefile
include tox.ini include tox.ini
graft Tests graft Tests
graft checks
graft patches
graft src graft src
graft depends graft depends
graft winbuild graft winbuild

View File

@ -75,7 +75,7 @@ debug:
.PHONY: release-test .PHONY: release-test
release-test: release-test:
python3 Tests/check_release_notes.py python3 checks/check_release_notes.py
python3 -m pip install -e .[tests] python3 -m pip install -e .[tests]
python3 selftest.py python3 selftest.py
python3 -m pytest Tests python3 -m pytest Tests

View File

@ -271,17 +271,13 @@ def _cached_hopper(mode: str) -> Image.Image:
im = hopper("L") im = hopper("L")
else: else:
im = hopper() im = hopper()
if mode.startswith("BGR;"): try:
with pytest.warns(DeprecationWarning): im = im.convert(mode)
im = im.convert(mode) except ImportError:
else: if mode == "LAB":
try: im = Image.open("Tests/images/hopper.Lab.tif")
im = im.convert(mode) else:
except ImportError: raise
if mode == "LAB":
im = Image.open("Tests/images/hopper.Lab.tif")
else:
raise
return im return im

View File

@ -0,0 +1,390 @@
/* XPM */
static const char *hopper[] = {
/* columns rows colors chars-per-pixel */
"128 128 256 2 ",
" c #0C0C0D",
". c #0A0708",
"X c #1C0A04",
"o c #120B0C",
"O c #170808",
"+ c #0B110D",
"@ c #16120C",
"# c #0D0D12",
"$ c #0D0D1A",
"% c #070A16",
"& c #120D13",
"* c #120E1A",
"= c #1A0C16",
"- c #0D1114",
"; c #0D121B",
": c #091518",
"> c #131215",
", c #14131B",
"< c #1A141C",
"1 c #1B191D",
"2 c #191517",
"3 c #250906",
"4 c #390904",
"5 c #27150A",
"6 c #250A18",
"7 c #251719",
"8 c #361410",
"9 c #342215",
"0 c #0C0C24",
"q c #0C0D2B",
"w c #060927",
"e c #130D24",
"r c #150D2A",
"t c #0C1225",
"y c #0C122C",
"u c #061227",
"i c #151422",
"p c #1A1522",
"a c #1C1B23",
"s c #13132C",
"d c #19172A",
"f c #0C0D35",
"g c #130E37",
"h c #0D1436",
"j c #131333",
"k c #13143C",
"l c #191838",
"z c #241926",
"x c #231B38",
"c c #2E1226",
"v c #372628",
"b c #292538",
"n c #362B37",
"m c #2F2A2F",
"M c #1A2233",
"N c #4C150D",
"B c #740F10",
"V c #512916",
"C c #793419",
"Z c #6D2C13",
"A c #4E1524",
"S c #741624",
"D c #4E332E",
"F c #6F3629",
"G c #574438",
"H c #744831",
"J c #775A2E",
"K c #0E1444",
"L c #141443",
"P c #1B1A44",
"I c #14144B",
"U c #1A1B4C",
"Y c #181747",
"T c #1B1B53",
"R c #181955",
"E c #0F0E44",
"W c #231C46",
"Q c #231C56",
"! c #1C234E",
"~ c #272547",
"^ c #2E2F52",
"/ c #2E3765",
"( c #483947",
") c #742D4A",
"_ c #364970",
"` c #534A51",
"' c #6E534D",
"] c #756654",
"[ c #53556D",
"{ c #6B5B69",
"} c #746B71",
"| c #5E616A",
" . c #880C15",
".. c #881217",
"X. c #8D0D0F",
"o. c #8B3218",
"O. c #8C3828",
"+. c #AC2F30",
"@. c #9A1825",
"#. c #CE202B",
"$. c #8A452A",
"%. c #974A2B",
"&. c #884934",
"*. c #954B35",
"=. c #995539",
"-. c #895736",
";. c #A75738",
":. c #A84E30",
">. c #996839",
",. c #B6683B",
"<. c #AE6835",
"1. c #A35419",
"2. c #D26D19",
"3. c #CC712E",
"4. c #CD6922",
"5. c #A83152",
"6. c #985845",
"7. c #8A5748",
"8. c #AE5A46",
"9. c #916A4F",
"0. c #A96647",
"q. c #B76947",
"w. c #BA744A",
"e. c #B97757",
"r. c #AB6F53",
"t. c #8D736D",
"y. c #B27669",
"u. c #91566F",
"i. c #C56B4A",
"p. c #C8764B",
"a. c #C87856",
"s. c #D47A59",
"d. c #C96E53",
"f. c #C77C64",
"g. c #D17969",
"h. c #D45D68",
"j. c #C52A46",
"k. c #D58932",
"l. c #B38355",
"z. c #968775",
"x. c #BA8667",
"c. c #B38C74",
"v. c #AB9C73",
"b. c #C9845A",
"n. c #D7855B",
"m. c #D39454",
"M. c #E28C5B",
"N. c #F7B251",
"B. c #C78867",
"V. c #D98866",
"C. c #D8956A",
"Z. c #C79878",
"A. c #D89876",
"S. c #CD8C70",
"D. c #E38A68",
"F. c #E5956A",
"G. c #E79776",
"H. c #ED9176",
"J. c #D6A371",
"K. c #E8A379",
"L. c #F3A677",
"P. c #D8A05D",
"I. c #3D65AB",
"U. c #3F67B2",
"Y. c #3B5C9C",
"T. c #506796",
"R. c #72748D",
"E. c #446AAE",
"W. c #4869A9",
"Q. c #4166B2",
"!. c #436BB3",
"~. c #496EB4",
"^. c #476DB9",
"/. c #4A71B6",
"(. c #4C73BA",
"). c #4772B6",
"_. c #5176BC",
"`. c #547BBD",
"'. c #577BB7",
"]. c #5572A9",
"[. c #6B7CAA",
"{. c #505B8C",
"}. c #557CC1",
"|. c #4C73C2",
" X c #897987",
".X c #9F7593",
"XX c #C46B87",
"oX c #5981BF",
"OX c #5884BD",
"+X c #768AB9",
"@X c #7288B5",
"#X c #5C83C3",
"$X c #5D8AC5",
"%X c #6186C5",
"&X c #648AC6",
"*X c #6B8DC6",
"=X c #668BC9",
"-X c #6B8ECA",
";X c #6586C6",
":X c #738DC7",
">X c #6D91CB",
",X c #6C94C6",
"<X c #7294CC",
"1X c #7895C8",
"2X c #6E92D1",
"3X c #7294D3",
"4X c #7698D5",
"5X c #708ED1",
"6X c #7799E3",
"7X c #9B9399",
"8X c #928890",
"9X c #B89887",
"0X c #A99191",
"qX c #B9A598",
"wX c #B1A394",
"eX c #8C8EAA",
"rX c #AB9AA6",
"tX c #ABA4A9",
"yX c #B7A9A8",
"uX c #B7ABB4",
"iX c #B6AFB7",
"pX c #C69B86",
"aX c #D4978B",
"sX c #EF9C83",
"dX c #CAA487",
"fX c #D7A787",
"gX c #C7A899",
"hX c #D1B294",
"jX c #E9A887",
"kX c #F8A886",
"lX c #F9B798",
"zX c #F1B291",
"xX c #C9B3AD",
"cX c #F4B9A5",
"vX c #D497B3",
"bX c #D5C6B1",
"nX c #FEC4A6",
"mX c #EAD0B2",
"MX c #EDD1A4",
"NX c #8399C8",
"BX c #B2B4CD",
"VX c #C7BBC7",
"CX c #D3CBCF",
"ZX c #ECDAD1",
"AX c #F6E6DA",
"SX c #F7EACF",
"DX c #D1D1E9",
"FX c #E7DDE4",
"GX c #E9E5E8",
"HX c #F7EAE6",
"JX c #FDF6E9",
"KX c #FEFCFE",
"LX c #FAF7F7",
"PX c #F1EBF6",
"IX c #DCE2E5",
"UX c #BEC5DF",
/* pixels */
"L k f k P l y j T R I I U U L U R Q T L E E E R R R E U j } GX9XfXpXxXR.j ~ ~ = V Z.G > b R.DXPXLXHXHXHXHXCX~ / T.Y.T.T.W.T.W.E.Q.I.E.I.I.E.E.I.I.I.I.I.Y.I.Q.^.Q.E.E.E.E.Q.Q.~.U.U.U.U.U.U.Q.Q.U.U.U.U.U.U.Q.Q.Q.Q.U.U.U.Q.~.~.Q.U.Q.~.^._._._._._._._._.(.(.(.",
"L k f L L k y h T R I L U U L U R R T L E E E R I R I U l XuX' fXV v [ / P h z V Z.G a y l [ 7XCXHXJXHXHXCXb ! {.{.T.{._ _ {.T.W.W.T.T.W.I.I.U.U.I.I.E.W.I.I.Q.Q.E.E.E.Q.Q.~.~.~.U.U.U.U.Q.Q.Q.Q.U.U.U.U.U.Q.~.~.~.U.U.U.U.U.Q.~.Q.Q.Q.~.~._._._.'.`._._._._._.",
"L k f L L k 0 h T T I E U U L T T T U L h h E U R R E U W R.{ D pXF z l L U ^ p F fXD i P W Y ~ n CXHXHXHXFX8Xl W ~ ~ l ^ b b ^ ^ / [ T.W._.U.^.U.U.Q.E.W.W.~.^.E.E.E.~.~.Q.~.~.~.~.~.~.~.~.Q.Q.Q.Q.U.U.U.U.U.U.~.Q.U.U.U.U.U.U.^.~.~.Q.~.~._._.`.`.`.`._._.|.|.",
"k k f L L k 0 h U T L h I U I T T T U k h k E U Q I E U k ` m ' hXV z k U I Q d V fX( j L U W W z VXLX8X XuX( z b x d ` X X` n n b b ! {.W.~.I.Q.Q.Q.E.W.].~.I.~.~.~.~.~.~.~.Q.~.~.~.~.~.~.~.~.~.Q.Q.Q.U.U.U.U.U.Q.U.U.~.~.Q.Q.Q.Q.Q.Q.Q.~._._._._._._._._.|.|.",
"k k f L L k q j U T L h U U I T R T U k h h E I E R I I k b p ' Z.V z k ! T U p H Z.c k U L U W n CXCXn z = c c v 7X8X` 8XPX} c R.tX` n b / {.].W.~.~.E.W.E.~.^.~.~.~.~.^.~.~.~.~.~.E.E.~.~.~.~.~.~.~.~.~.~.U.U.~.U.~.~.~.~.~.Q.U.Q.~.~.E.~.~./././.(.(.(.(.(.(.",
"h k h P L k q j U T L h U U I T R T U h h h E E I R I E k d p ' dXV x P U L L z J fXv l L L P j n IX` m = = 7 ' HXLXKXCX7XKXtXrXLXKXJXqXv n ^ {.T.T.T.W.].E.Q.^.~.~.~.~.^.^.~.~.E.E.E.E.E.~.~.~.~.~.~.~.~.~.U.U.~.~.~.~.U.U.U.U.Q.Q.~.~.E.~.~.E.~.~.~./././.(.(.",
"j k k P Y k q h U U k h U R I R R T U h h h E E R R E I Y d d ' Z.V p L ! ! Y = H Z.v j h ! l b n iXtX7Xa p t.LXZX0XHXPXKXKXPXLXLXCXbXAXVXn m 8XVXeX[.T.W.W.^.^.~.~.~.^.^.^.^.~.E.E.E.E.E.~.U.U.~.~.~.~.~.~.~.~.~.~.~.U.I.I.I.~.~.Q.Q.~.~.~.~.E.~.~.~./.(.(.(.(.",
"j k k U P k 0 y L U k h U R I R T Q T E f E E E E I I I f k z ` Z.V d U T L P z >.B.c j l l l } IXKXKX8Xp ` t.` t.' G ] tXIXIXwX] ' z.t.` { c n PXKXLXUX+X].E.~.~.~.~.~.^.^.^.~.Q.E.E.E.E.U.U.U.~.~.~.~.~.~.~.~.~.~.I.I.I.I.~.~.~.Q.Q.Q.~.~.~.~.~.~.~./.(.(.(.(.",
"j k R.~ k k q q Y T k f Q T I I I Q I L E L E E R I I E f d x ` dXV d T T T U z -.Z.c b s b CXLXKXLXKX} z 7 7 z.hXSXSXz.AXbXmXbX0XJXmXmX` 1 n b iXKXLXLXLXDX@XT.].W.E.(.U.|.^.^.~.~.W.E.~.~.~.~.~.~.U.~.~.~.~.~.~.U.I.I.I.U.~.~.Q.~.~.~.~.E.~.~.~.~.~.~.~.~.^.^.",
"j ~ DX[ W q s q Q I f h Q L R R R R I I L f E I I I I L P d x ` dXV d R R T Y z -.Z.7 0 ` GXLXKXJXLXJX` 7 7 7 t.hXMXmXJXLXJXJXJXJXSXSXmX' n z b 7XKXKXPXKXLXPXBX].T.W./.^.^.U.|.~.~.~.~.~.~.~.^.~.U.U.~.~.~.~.~.~.~.U.U.I.U.~.~.~.~.~.~.E.E.~.~.~.~.~.~.~.~.^.^.",
"r x DXIX~ s $ b L U L L Y Q L I I I T L f L U E T T L k k d x ` dXV x T R U L p 9.Z.7 | KXKXLXLXJXSXZXD v 7 7 D mXMXmXSXZXZXCXZXAXZXdXmXG v n n 7XKXLXKXKXKXLXKXDX[.T.]./.I.}.U.~.~.~.~.~.~.~.U.~.U.U.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.~.~.~.~.^.^.^.^.",
"r b CXPX X[ iX[ Y U L k P [.~ k U T U L f f f L I U U k f d x ` dXV z T T U L z 9.x.D LXHXJXJXZXqXqXmXD @ 7 7 9 ] mXbXJXKXKXKXLXJXJXMXv.9 7 7 7 } HXKXKXHXLXJXLXKXDX[.T.W.(.~.^.Q.Q.E.E.E.~.U.U.~.U.U.U.~.U.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.~.~.~.~.~.^.^.^.",
"d x VXKXPXGXCX` P U L Y ~ BX| l k P k k k P w k h L L P j d d ( dXZ z P ! L k z 9.B.mXJXJXSX9Xz.t.D 5 5 5 7 7 9 hXmXv.mXLXKXKXKXJXSXv.mX] v c v D t.xXZXJXJXJXLXPXKXUXT.W.E.~.~.Q.Q.E.E.E.E.Q.Q.~.I.I.~.~.E.E.~.~.~.~.~.~.~.~.~.~.~.~.I.~.~.U.U.~.~.~.~.^.^.^.(.",
"x 8XGXPXHXHXtXb k U U k l CXtXd b ~ | {.j q k f P / h k k d d ( dXF < k ! L k z 7.zXSXJXSXt.] V 3 3 X 5 @ 2 c 7 z.v.bXSXAXKXLXLXZXmXhXMX' 7 n 7 9 3 8 ] qXZXJXLXLXKXPX@X].W.I.^.~.~.~.E.E.~.~.~.E.I.E.~.~.E.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.U.U.U.~.~.~.~.^.^.(.(.",
"uXGXHXLXJXAX} & W Q g g ~ DXCX` [ VXDX[ s j s y ^ eX~ j j d l ( pXF 7 k ! L L z 7.nXJXAX] D 3 3 3 X ' ] 7 1 = 9 t.SXSXMX9XZXJXJXxXmXSXSXJ v v 7 9 ] 9 9 5 ' 9XxXJXHXGXDX{.'.).~.~.~.E.E.E.E.~.~.~.~.~.~.~.E.I.~.~.~.~.~.~.I.I.~.~.~.~.~.~.~.U.~.U.U.~.~.^.(.(.(.",
"iXFXPXLXLXLXyX( k W k ~ b CXGXFXPXGXtXl l s 0 j ^ DX` d d d x D pXF z P T L P z ' AXAXz.5 X 3 9.] 9 5 v.5 G ` 9 J hXhXhXmX9X' ] qXhXhXMX] 9 D 9 G z.5 ] t.8 8 G wXHXPXIX[.T.W.].~.~.~.~.E.E.~.~.~.~.~.~.~.E.E.~.I.~.~.~.~.I.I.I.~.~.~.~.U.U.U.~.U.U.U.~.^.(.(.(.",
"d n } LXLXCXVX[ W W d ` tXHXAXAXHXIX^ x j l w s ` GX7X7 n } ~ D dXH p k ! P k l ` xX8X2 @ 5 7 gXbXhXhXv.hXmXSXmXMX9.J 5 5 V 9 9 9 V G dXhXSXSXdXhXl.MXMXdXV J V 9 wXLXFXtXR.T.T.W.~.^.~./.E.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.).~.~.~.^.^.^.^.~.^.(.(.(.",
"d d [ LXVX( ^ ~ k ^ 7XFXLXHXJXHXAX} x l k w l i ` GXCX8XbX Xx v Z.H z k ! P d d i . & @ . 2 7 v z.v.V dXmXdXZ.mXSXSXSXbXt.` 7 D ] bXJXSXSXSXMXSXSXl.hXMXhXmXMXV 5 v xXxX} ^ ! {.W.~.^.U.).E.E.E.~.~.E.E.~.~.~.E.~.~.~.~.~.~.~.~.~.~.).).).).(.(.(.(.(.(.(.(.(.(.",
"f ~ ` PXR.l l j Y ~ { uXFXLXJXHXFXuX~ W f f d a } HXZXxXyXn d n Z.H 7 j P l j r p & o @ @ @ o 7 X 9 D ] V 5 hXbXqXv.] G D ` n ` G ' 0X9XmXmXz.G 9.9.3 8 ] hXgX9XwXv D z > $ l ! W.~.^.U.).E.~.E.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.^.~.)./.(././.(.(.(.(.(.(.(.(.(.(.",
"g W ^ DX( l l q L W r b ` CXLXLXPXPXR.k k ^ | 8XCXHXbXxX{ < d v Z.J 7 j l d r * . > 2 o . @ 2 = 7 X X 5 D 5 5 5 9 9 9 @ 7 7 2 v 7 7 v 9 v V D G qXgXxXD 3 3 ' z.D 9 2 > 1 # d u Y.W.~.~.).E.W.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.^.~.)././././.(.(.(.(.(.(.(.(.(.(.",
"U U / eXP P l f L L d r b CXPXR. XUXDX/ k ~ ` 7XCXZXZXyXv p k v B.-.o d l i * * & o o 2 @ . & . < & o 7 o 7 2 @ @ @ 7 2 v < z < z 7 2 7 9 7 5 9 ] z.ZXCX` 7 5 X @ @ & . o # % u _ W.E.E.E.E.E././././././.~.~.~.~.~.~.~.~.~.~.~././.(.(./.(.(.(.(.(.(.(.(.(.(.(.",
"f U ~ / L U k f U Y k x d DXVX~ x W {.[ f d 2 7 t.ZXZXxX} x k z x.-.3 d a $ & & . 2 o . @ . # p # , & . > & o z 7 2 2 2 o & < & < . > 7 > 7 7 7 v ' m 7 @ 2 . @ . + . > . > % y _ W.E.E.E.E.~./././.(.(././.~.~.(.(.~./.~.~.~.~.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.",
"I T P P ^ ~ k k L U q l ~ DX{.W W Q Q ~ d * * o { HXxXVXCX^ q z x.>.5 i , # & & > o > . + + > . # . * * . < & . o o 2 7 & 2 2 2 > > < > 2 7 o o @ . @ . o . . + . . # & . . ; u / ].W././././.(././.(.(././././.(.(.(.(.(.(././.(.(.(.(.(.(.(.(.(.(.(.(.(.(.}.}.",
"E Q L P Q Q P f f L k k ^ BXU ~ P W T Y j i * * XFX` b 7XR.l 7 l.>.7 , # # < o o . > . - - - $ # # . & , . & . o o o . . o . o o . . . . . o o o @ . . . . 2 . + . - . > > . w _ ].W./././.~.(./.(.(.(.(././.(.(.(.(.(.(.(.(.(.(./.(.(.).).(.(.(.(.(.(.(.(.(.}.",
"I U U U T Q k h L L h P Q / T L U T T U j 0 0 r 7XuXd r d ^ l < r.0.5 ; - - - & . > # # - + % # . . + . # # . . . . . . . . . . & # . . o o o o . o o . . . . . + . # # . > ; w _ ].]./.E.(.(.}.(.(.(.(./././././.(.(.(.(.(.(.(././././.(.(.(.(._.(.(.(.(.(.(.(.",
"L U U U T Q k q k L k P U Q U U U T T U k q q r X( j d 0 t y 7 r.0.@ ; + - - & . > & . . - - , - + + + . . + > . . . . . . . . . . . . o o o o o o o . . . . . . . % . . . - t / ].].]./.`.(.|.(.(._._.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.(.",
"L U U T T Q k q k k k P I I I T R R T U L f q f / L W q w s s 2 0.r.X ; % - # # > > . # > + . . . . . @ @ o . o o o o o o o o o o o o o o o X X o o o o o . # # - . # > o . # w ^ ].].'./.`.(.}.(._.`.`.`._._._.`.`.`.`._.(.(.(._._.(.(.(.(.(.(.(.(.(.(.(.(.(.(.",
"L U I T R Q j q k k h L I I R T R R T U L f q f E T U f j j 0 7 0.l.X ; % - # . . # . . . . o @ X X X X X X X 3 3 3 3 3 o o 3 3 o o 3 3 3 3 3 3 3 3 X X o X o o . . . & o o - % ! ].].'./.(.(.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.(.`.`._.(.(.(.(.(.(.(.(.(.(.(.(.(.",
"k U U T R Q k q l k f L T I T T R R T U k f 0 q I R E L q q q 7 >.l.X , % + . . . & = o @ X X X 3 4 N N N V V V V N N N N N N N N N V V F F H H H H F V 8 3 3 3 o . & o . . > % P ].].'./.'._.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.(.(.(._._.`.(.(.(.(.(.(._._.",
"k P U T R Q k q k k k L T I T T R R T U k q 0 q I I T f w j j 6 >.r.3 - . + + . > . X @ X X 3 N F 6.r.y.y.y.y.y.y.r.r.0.6.7.6.7.7.6.6.0.r.y.B.B.y.y.x.x.y.7.V 3 X . & & @ . # % h ].].'.'.'.`.}.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`._._._._.(._._.}.",
"k P U T Q Q k q f l k L T E T T R R R U j 0 0 y k E I L k j q 7 0.<.3 # . + . . o o X X 3 9 ' c.Z.A.aXaXaXaXjXjXjXA.A.A.S.B.S.B.S.S.A.A.G.K.K.G.A.C.C.Z.A.Z.r.' 9 o X o o @ - ; u ].].'.'.'.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.`.`.`.`.`._._._.}.}.",
"d W U R R Q k q f k g L T I Q R T R R U j 0 0 y k L I I f f f 6 r.>.3 # . . . . X 7 7 8 G t.pXaXaXA.A.fXzXzXzXlXzXjXzXlXzXzXzXzXkXzXkXzXlXlXzXjXK.K.K.A.A.K.Z.c.t.G 5 3 X o . t u '.'.'.'.'.'.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.`.}.}.`.`.`.`.`.`.}.}.}.}.",
"` b U Q R Q k q q l L L L T T R T T R U k 0 0 t j U I E L f f 7 r.r.X . . @ o o v ' F H y.Z.fXfXK.jXjXzXzXzXzXlXcXlXzXlXzXzXnXcXzXlXlXlXzXnXnXcXlXjXzXA.jXA.J.B.c.t.-.D 3 X & % K '.'.].'.'.oX`.oX`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.`.}.}.}.#X#X",
"xXz W Q I _ ~ y j j f U I I U U U R T U k q 0 t k U I L L f f = 0.l.X @ @ . . 9 ' ' H 0.B.A.fXfXjXjXzXzXzXlXcXcXzXzXlXnXcXzXzXzXcXcXlXcXzXzXzXzXzXcXjXA.G.A.C.B.Z.y.e.-.D o @ % K '.'./.'.'.'.oXoXoX`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.`.}.}.}.}.}.}.}.}.}.}.",
"HX7Xx ^ eXBXM $ l x Y U R R I R I I T U k q 0 y k U I I k j j = 0.l.X > o o 7 D ' H 7.y.B.A.fXfXjXzXzXzXzXzXjXjXzXzXlXnXlXzXzXzXjXzXzXsXsXD.B.e.x.x.S.A.B.B.S.Z.Z.c.l.e.' 7 @ , ! [.'.`.'.'.oX#XoXoXoXoXoXoXoXoXoXoXoXoXoX`.`.`.`.}.}.#X#X}.#X#X}.}.}.#X}.#X}.}.",
"AXZX{ CXPX| d 0 ` R.d Y U R I U E L R U k q q y L U U U k h l o r.l.X > . o v D H H r.x.l.B.A.A.B.B.B.e.e.e.B.S.jXzXzXlXzXzXcXzXfXjXjXD.D.a.q.e.r.e.Z.A.jXA.Z.Z.Z.c.e.e.9.G 5 # ^ [.'.`.'.'.'.'.oXoXoXoX#X#XoXoX#XoXoXoXoXoXoXoX#X#X#X#X#X#X#X#X}.}.#X#X#X#X#X#X",
"ZXHXHXGXVXb i i ` FX^ W Y g ~ P k L U I k q q q L U I U k q y @ >.l.X $ & 7 9 F ' 7.r.e.x.Z.A.A.A.V.a.a.a.f.B.A.A.jXzXzXzXzXlXzXzXfXA.D.D.8.*.=.*.6.r.B.fXaXfXc.c.Z.B.9.t.` v 7 ^ @XoX'.'.#X%X}.#XoXoXoX#X#X#XoXoXoXoXoX#X#X#XoX#X#X#X#X#X#X}.}.#X#X}.}.#X#X$X$X",
"HXAXZXFX{ x b # { FXrXx x {.eX^ k L U L k q 0 q f L E E f 0 t @ >.<.X , & 7 G 7.7.9.r.x.Z.Z.S.S.B.e.0.6.6.0.0.0.B.A.jXK.A.jXlXzXjXfXA.g.i.:.%.*.O.Z F Z Z F F F H ' -.7.t.` v c [ @X@X'.oX=X#X#XoXoXoX#X#X#X#XoX#XoXoX#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X;X;X",
"ZXAXZXGX` b & & ` ZXHX} uXGX8Xl k L U L j r r 0 r W Y f q 0 i 5 w.>.5 , . 9 7.9.-.-.9.c.c.x.B.B.x.r.9.7.7.6.r.y.r.B.A.jXG.jXlXlXzXK.B.e.0.=.C N Z &.6.*.&.H N V t.' V F ' { v v [ @XoX'.oX#X`.#X#X#XoX#X#X#X#XoX#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X#X$X",
"AXJXHXFXIX^ x = ] ZXAXAXCXVXb j k L U Y d i & & x ^ ~ f q q i X 0.<.5 & 7 D 9.9.-.J H ' G V V N 4 8 N N N N V Z $.r.G.lXlXzXlXnXlXK.b.0.C C 6.B.r.y.y.S.r.c.SX9.8 V A D ` ' D D R.eX+X%X$XoXoX$X#X#X#X#X#X#X#XoX&X#X#X#X#X#X#X#X#X#X#X#X%X%X#X#X#X#X#X#X#X#X#X#X",
"gXAXCXFXPXuXz D bXAXSXZXCX{ b k L L Y W r ` n v 7XtXx j w r r 3 0.>.8 2 9 ] y.9.H F 8 D D V N 0.cXaXy.r.r.B.S.*.O.Z <.n.kXkXL.L.F.p.;.0.jXy.9.V N N F F r.r.c.MXD V ' F D ' u.' XR.+X%X$X$XoX#X#X#X#X#X#X#X#X#X#X#X%X&X&X%X#X#X#X#X#X%X%X%X#X#X%X#X#X#X#X#X;X;X",
"z.ZX` ( { eX{ n CXZXZXZXxXm x k L U Y W r ` } z.iX` r w r r 0 3 0.>.8 D G 9.r.-.F V 3 8 3 N r.y.y.7.F V V N &.B.a.w.p.p.F.F.F.b.p.,.,.q.0.6.V H N V V N C $.H C H F V N V H r.9XgXrX[.*X&XOX&X=X%X%X%X#X#X#X#X#X&X&X&X&X&X&X%X%X%X%X%X%X%X%X%X#X#X#X#X%X%X;X=X=X",
"z.xXx ~ f x x z t.ZXAXAXCX} x Y U T U k p v ] ] } z r r 0 r s 6 r.r.8 D 9.r.6.C V V D D 8 7.9.r.' V N N N N Z F 0.;.;.s.n.p.a.p.3.1.p.p.;.Z O.V 4 N N N B o.o.%.0.-.H H &.r.f.6.y.yX@X:X'.oX-X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X%X%X%X%X#X%X%X%X&X=X=X=X",
"} } k W U k 0 & v CXAXCXGXCX~ f L T U k z D t.] 9 o p d w r d = >.l.N D c.e.0.6.F H V F V H F 6.V N V 4 4 N &.6.C O.%.s.i.F.zXkXF.n.M.s.;.i.8.=.&.O.F O.%.%.;.q.B.e.b.w.;.;.q.o.Z 0XeX:X[.@X-X'.&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X&X%X=X=X=X=X=X%X%X",
"[ ( Q L I U r * 7 CX8Xv } iXR.j L T U k r ( } t.{ . r r s 0 r 6 >.w.N ' y.$.$.=.6.6.6.r.6.$.O.O.O.C O.O.=.=.=.w.d.n.n.M.:.G.lXlXL.,.H.H.D.d.q.g.a.q.0.,.q.d.s.p.a.b.C.w.<.;.d.s.Z t. X[.:X;X;X=X&X&X&X&X&X&X&X&X=X=X=X=X&X&X&X&X&X&X&X=X=X=X=X=X&X=X=X=X=X;X;X%X",
"~ k L T T U 0 $ b CX` x x x ^ k L L U P d ( ' } 7X` r r s s 0 o 0.w.4 7.6.O.8.8.0.6.B.B.0.;.o.o.o.o.:.s.G.V.s.V.s.H.kXF.%.G.lXlXlXF.H.L.D.s.H.G.kXzXsXD.s.D.F.n.V.V.V.w.<.n.V.a.O.y. X[.:X;X;X2X=X=X=X=X=X=X=X=X=X=X=X=X=X&X&X&X&X&X=X=X=X=X=X=X%X=X=X=X=X=X=X=X",
"f k Y U L k 0 $ b 7Xj W Q L k q L U U Y r p 2 7 [ } d d 0 s t o r.r.4 F &.o.q.8.=.;.a.C.w.w.:.O.O.;.d.sXsXkXH.s.i.L.lXs.q.kXlXzXlXK.a.kXkXV.a.G.L.H.D.M.p.D.F.V.A.A.A.B.q.D.kXV.%.y.eXoX@X*X$X=X=X=X=X=X=X=X=X-X=X=X=X=X=X=X&X&X&X&X=X=X=X=X=X=X=X=X=X=X=X=X=X=X",
"k j L k P P q 0 x ^ Y Q R R L k h U U k j * # * p x k l q 0 t @ >.w.8 V 6.C a.g.q.%.w.n.p.p.d.q.:.i.i.V.s.i.q.d.lXkXH.,.V.lXlXlXzXlXs.G.K.lXkXn.n.i.p.i.p.p.G.C.B.V.B.B.a.q.V.A.=..X@X*XoX-X=X$X=X=X=X=X>X-X-X>X-X-X-X-X-X=X=X&X-X-X-X=X=X=X&X&X=X=X=X=X=X=X=X=X",
"j k L U U k q 0 j j Y U U L L L Y W L k y w ; $ 0 q h k q y y o >.w.4 8 r.O.V.s.8.%.d.s.n.p.n.q.%.i.p.a.a.f.sXlXlXzXF.q.kXlXnXcXnXnXjXB.zXlXlXnXlXkXG.V.V.G.L.F.V.n.b.b.8.o.i.D.r.t.@X+X@X*X-X>X>X-X*X*X*X-X*X*X*X*X-X>X-X*X&X*X-X-X-X-X=X=X&X&X=X=X=X=X=X=X=X=X",
"j k L U U k q 0 j f k Y Y k k L U W L k y u t 0 w j f f k y s 3 0.l.3 V r.=.D.s.:.%.i.i.n.n.V.q.,.s.G.kXlXnXnXcXlXnXb.C.zXlXcXnXnXlXnXS.G.zXlXlXlXlXzXK.K.K.F.V.V.n.,.C.d.o.;.S.c.8XeX:X@X;X-X=X>X-X*X*X*X-X*X*X>X-X-X-X-X*X*X*X-X-X-X-X=X=X=X=X=X=X=X=X=X-X-X=X",
"j k L L P j 0 0 j k L P Y k k L U U L h y 0 r r f f f f k w i 5 >.w.V 9XcXe.V.V.%.q.s.i.p.n.b.w.:.,.L.nXnXnXnXnXnXlXq.jXlXlXcXnXnXlXlXjXq.sXzXzXlXlXK.F.G.F.V.n.V.p.w.C.sX8.q.f.9X8X+X*X*X-X2X-X>X-X-X-X-X-X-X-X>X-X-X-X-X-X*X>X-X-X-X-X-X=X=X=X=X=X-X-X-X-X-X-X",
"h k L L P j 0 0 j k Y U P Y k Y Y U k h y w r r j r j f y t r X C C.c.hXcXA.K.p.w.A.C.q.<.p.a.b.p.i.F.lXcXnXlXzXjXq.q.G.kXlXcXlXnXnXnXzX0.:.s.H.G.G.G.K.kXkXF.n.a.e.b.C.kXD.q.y.0XeX+X*X*X-X-X=X>X>X-X-X-X-X-X-X>X-X-X-X>X-X-X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"h k L L k s $ $ y j L Y Y L L Y Y U k h y 0 r r r f k r r d & 9 w.C.c.hXcXA.G.n.C.jXG.q.<.p.w.b.D.i.p.D.kXkXV.i.;.:.D.H.kXlXnXcXcXlXlXlXsXi.8.8.:.;.a.G.G.G.F.a.a.a.l.C.kXs.q.S.8X@X:X>X-X-X>X=X>X-X-X-X-X>X-X-X-X-X*X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"k k L L l t $ $ y k L Y L L L Y Y P k h y 0 r r r f f s r # 7 t.J.P.c.hXMXfXC.G.C.K.K.a.q.i.w.p.F.M.i.:.%.;.;.:.o.s.kXH.kXzXcXlXlXlXzXzXsXH.8.H.H.H.sXkXD.V.V.a.b.e.B.A.G.q.V.B.8X@X*X-X-X-X2X2X>X>X-X-X>X>X>X-X-X-X-X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"k L L L l t % $ y k L I P P L L L Y h h y y s q r r r s * & ' hXK.J.x.hXcXx.B.K.C.A.J.b.i.i.p.b.a.F.D.s.V.H.V.i.:.H.D.V.G.sXzXzXjXG.sXV.s.s.:.q.H.kXkXH.D.n.n.V.b.e.B.A.b.V.G.c.eX:X>X-X&X*X*X&X>X>X-X-X>X>X>X>X>X-X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"k k L k j t # ; y k L I L L L k g L f h y y s 0 q r s r * D wXgXJ.P.x.cXMXl.b.C.jXA.C.e.q.i.a.b.p.n.V.H.sXkXV.%.o.;.O.%.q.q.f.B.B.f.8.:.Z O.B O.s.G.H.D.H.V.V.C.B.e.B.0.C.K.s.pXeX&X=X-X,X,X<X*X>X>X-X-X>X>X>X>X>X>X>X>X>X-X-X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"f P k f ~ 0 $ ; j w I T L L f L Y P k y y y y % 0 w y i ` 0XgXhXP.P.x.cXhXl.x.A.b.b.%.w.p.i.a.n.p.n.V.D.G.V.:.o.V.q.Z Z C o.$.$.$.O.=.o.%.g.:.B :.s.g.s.V.n.V.C.V.B.e.q.q.w.A.9X+X-X=X>X*X*X*X,X*X>X>X>X>X-X>X>X<X>X>X-X-X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X2X3X3X",
"d j f W s s $ % s k g L L k h g g k f y t t t t w y t | tXwXgXfXP.P.B.cXhXc.x.A.C.B.C.K.V.i.p.n.n.a.F.V.V.i.o.i.G.G.V.V.V.0.o.N C 8.g.V.g.D.i.Z B ;.d.s.s.s.V.C.B.C.kXG.K.C.fX0X+X=X=X-X*X*X*X:X*X>X>X>X>X-X>X>X>X>X>X-X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X>X",
"p d W / d $ i i 0 k P U P k L k g f q q i i $ i $ d 8XiXrXgXgXfXP.P.B.cXhXZ.x.S.jXA.jXkXsXa.p.n.F.n.n.V.i.O.:.D.H.D.H.V.H.H.g.q.a.V.s.V.s.a.V.q.B o.:.,.i.s.V.C.V.C.jXzXC.fXgX8X+X*X-X>X,X*X:X,X*X>X>X>X-X-X>X>X>X-X-X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X>X",
"( m X[ d s $ $ l q j k k k h f g k j 0 i , & * x 7XiXtXyXqXgXdXP.P.B.cXhXZ.r.e.jXzXjXlXjXa.b.V.C.n.a.p.%.o.s.D.H.H.G.H.G.G.sXH.sXH.V.V.D.V.H.s.O.B O.;.a.V.s.b.b.A.zXjXK.dX0X8X+X*X-X>X,X:X:X,X*X>X>X>X-X-X-X>X-X-X-X*X*X*X*X*X-X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"qX0X8Xx r r * $ 0 s j j j f g k g j j r * & 6 D XrXuXyXyXgXgXfXP.P.B.zXhXZ.9.=.A.jXjXzXr.e.V.b.F.b.n.<.o.:.s.D.G.sXsXkXsXkXlXzXzXsXsXD.H.G.G.V.s.O.o.:.i.s.V.C.B.B.zXjXB.c.7XeX+X*X-X>X>X*X*X-X-X>X>X>X-X-X-X>X-X-X*X*X*X*X-X-X-X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X",
"mXqX} z p z $ * b ` b j l w k k h f s i = 6 A u.rXrXyXxXqXqXxXfXJ.P.B.zXhXZ.-.H >.B.A.0.N -.e.C.C.C.n.:.o.p.d.H.G.sXG.kXsXkXlXzXzXkXsXsXsXG.G.V.V.i.o.d.n.V.F.b.B.*.r.B.e.9XeX@X+X*X-X-X-X*X*X-X-X-X>X>X-X-X-X>X-X-X-X-X-X-X-X-X*X*X*X*X-X-X*X*X-X-X-X-X-X-X-X-X",
"hXqXD z z e * ( R.[ d $ d s q q h h j r 6 c A u..XvXxXyXqXqXxXdXP.P.x.hXzXZ.7.H -.0.0.Z 4 H w.V.V.G.V.,.:.s.s.D.s.a.q.d.i.d.H.H.g.s.i.s.s.a.s.D.s.V.;.s.H.F.G.V.e.C F 6.fXwX+X+X+X+X-X-X-X*X-X-X-X-X>X-X-X*X-X>X>X>X-X-X-X-X>X>X*X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X",
"bXqX( z = p & ` } p d d 0 s q k q h j $ 6 c A &.y.gXxXxXyXqXgXhXP.P.B.fXzXZ.9.H H =.x.9.V V e.C.C.F.kXs.i.d.s.d.%.o.o.o.o.o.:.:.o.o.o.o.o.o.o.:.q.d.D.F.kXF.n.B.B.C *.c.c.z.eX+X:X:X-X5X-X-X>X>X-X-X>X-X-X-X-X>X>X>X>X-X-X>X>X>X-X*X*X*X*X*X-X*X*X-X-X-X-X-X-X-X",
"ZXxX.Xz z = . ` n x d s l q q h f j s r = A S S r.9XgXyXxXqXgXfXC.F.m.fXfXe.6.H F V D D V N 0.b.V.G.L.L.i.i.:.O.o.%.:.;.8.8.+.+.:.d.H.D.s.D.n.a.i.s.n.L.L.F.S.Z.e.H -.y.qXtX@X,X*X-X5X5X5X>X>X>X>X,X,X,X,X,X-X-X<X>X,X*X>X>X*X*X-X-X-X>X-X-X-X*X*X-X-X-X-X-X-X2X",
"0XyXuX( = p . { ^ * d j j h y s r j s r 6 A S B 6.pXgXyXgXgXgXfXF.F.m.jXZ.0.>.;.Z N 3 9 v V =.b.b.n.G.L.V.p.a.p.s.s.g.H.sXsXH.sXsXH.G.sXH.D.s.n.V.p.F.L.F.n.A.B.9.H 9.c.yXtX+X+X-X2X5X5X5X-X>X>X>X>X,X,X>X>X-X-X>X,X,X*X*X*X*X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X",
"` = { } z & < 7X{ * d r q j 0 s s r y r 6 A S B ..g.gXgXgXgXgXfXF.P.B.fXZ.>.;.,.<.Z N N N N F w.p.n.L.F.n.p.M.n.s.n.n.V.H.sXsXH.H.D.H.V.d.p.n.F.F.p.F.L.n.n.B.x.-.H 9.0XtXeX+X<X2X>X>X>X*X,X>X>X>X>X>X>X>X-X-X-X*X*X,X*X*X*X*X,X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X>X",
"z p r z r * & } Xx d r r s 0 s s s t * 6 N S B B r.gXgXgXgXhXfXP.P.B.J.Z.0.,.<.3.3.<.Z N N Z 0.a.b.V.F.M.3.n.M.s.p.p.p.i.q.8.;.:.:.8.q.p.D.F.V.V.a.M.M.M.a.B.r.J -.t.8X7X@X:X<X2X>X>X,X,X,X>X>X<X>X>X>X>X>X-X-X*X*X*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"i d r s r * $ < } b d r d r 0 d y s i & 3 N B B B O.aXgXgXgXhXA.P.P.l.J.Z.0.,.<.3.2.k.k.<.Z N *.w.p.p.F.n.p.n.n.F.D.n.d.,.;.;.;.;.:.;.<.p.n.n.p.V.V.n.n.s.a.y.7.7.9.} 8XeX:X<X-X<X>X>X>X>X>X<X5X<X<X>X>X>X>X>X>X<X>X*X*X*X*X*X*X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"y q j q $ r * * b x d r d q i i t i = 6 N B B B B ..h.aXgXgXhXZ.P.m.b.J.A.>.<.<.3.2.2.2.3.1.B *.q.n.p.p.,.n.D.s.p.s.s.a.V.G.G.G.D.V.V.a.p.p.p.n.C.V.s.D.i.a.y.H 9.t.} eX+X1X<X>X>X>X,X>X>X<X3X5X<X<X<X>X>X>X>X>X<X>X>X*X>X>X>X*X>X-X-X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"l w h l s $ . & $ i s q j q y 0 t * 6 A B B B B B .+.aXaXaXgXZ.P.m.b.K.A.<.,.3.4.2.2.2.4.1...O.d.w.q.a.n.n.D.F.F.D.V.H.zXzXzXzXzXL.kXkXL.L.L.L.C.a.n.d.s.q.B.' } 8XeX+X+X,X,X,X,X,X,X>X2X3X3X3X<X<X<X<X>X<X<X<X<X<X>X>X>X>X>X>X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X-X",
"y l h s $ $ * & i 0 j f h q t ; , o 4 A S .B B B .. .g.aXaXgXZ.m.P.m.G.C.q.3.4.4.2.2.2.4.4.o.o.s.q.p.p.V.a.n.V.s.D.D.D.L.lXlXlXzXH.kXkXL.L.F.L.B.b.a.;.a.e.c.t.} eXeX+X>X>X,X,X,X,X,X2X2X3X3X3X<X<X<X<X<X<X<X<X>X>X<X>X>X>X>X<X>X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X",
"y h j s r $ # # i t q y h q t , = c A S . .B B .X...+.aXpXpXx.k.N.m.n.n.,.p.4.4.2.2.2.4.3.Z B e.b.q.w.p.b.p.n.n.M.n.n.F.F.L.kXH.G.kXzXkXL.C.C.e.0.;.q.q.r.y.t. XeX:X1X3X2X1X,X,X,X,X>X2X2X3X3X<X<X<X<X<X<X<X<X>X>X<X<X>X>X>X>X<X>X>X>X>X-X-X-X-X-X-X-X-X-X-X-X",
"y s s s r $ # # $ d l q q q i = c A S ..X.X.B B .X...@.f.S.aXe.k.N.k.p.p.<.3.4.3.2.2.2.3.,.Z F A.b.;.;.w.p.,.p.n.n.n.p.n.n.F.V.V.D.G.A.F.F.n.e.0.$.%.o.%.x.y.D 7X[.+X5X3X3X-X>X,X,X,X,X>X>X<X3X<X<X<X<X<X<X<X<X<X<X<X>X>X>X>X<X<X>X>X>X-X-X-X-X-X-X-X-X-X-X-X-X",
"r s s i r $ # # ; d j q r d = 6 A S S ..X.X. .X. .....B :.S.aXe.k.N.k.<.<.<.3.<.2.2.2.k.<.$.8 gXaXB.r.%.$.%.%.;.a.n.b.a.s.e.e.q.e.g.B.f.a.a.q.=.F C Z C r.Z.t.o ^ R.+X5X3X6X6X2X>X,X,X,X>X>X<X<X<X<X<X<X<X<X<X<X<X<X<X>X>X>X<X<X<X<X>X>X-X-X-X-X-X-X-X-X-X-X-X-X",
"r s s r r $ # - ; i q r r 6 6 A S S ....X.X. . . ..... .o.g.S.b.k.N.k.<.<.1.3.3.k.2.2.3.;.4 9 AXc.S.f.*.Z N Z C =.0.q.0.=.$.&.=.6.6.6.=.=.%.%.o.Z N Z r.B.Z.t.X < M T.:X5X3X3X4X3X<X,X,X,X,X<X<X<X<X<X<X<X<X<X<X<X<X<X>X>X>X>X<X<X<X>X>X-X-X-X-X-X-X-X-X-X-X-X-X",
"s s s r r $ # - ; i r r 6 6 N S S .X.X.X.X. .B . . . .B i.V.b.k.N.k.<.<.:.3.3.3.4.k.<.4 X t.bX9.S.f.e.$.N N N Z F F Z V N V F F V N N Z Z C o.Z Z 0.B.B.Z.t.@ > $ y {.NX3X3X-X3X3X<X,X1X1X1X<X<X<X<X<X<X<X<X<X<X<X<X>X>X>X>X>X<X<X>X>X>X-X-X-X-X-X-X-X-X-X-X-X",
"j j j r 0 $ # ; i p p 6 8 A S .. .X.X.X.X. . .B B B ..B X.:.D.,.k.N.k.1.:.%.p.<.,.<.-.8 X X wXgXH S.f.f.r.C C Z Z Z N 4 4 8 8 4 4 4 4 4 N C $.$.F 6.f.a.Z.Z.t.o . - $ y [ +X1X6X3X4X4X<X1X1X<X>X<X<X<X<X<X<X<X<X<X<X<X<X<X>X>X>X<X<X<X>X>X>X>X-X>X>X>X>X2X2X2X2X",
"j j j q 0 $ # ; , * c c A S .. . .X.X.X.X. .B B B B .. .X.+.D.;.k.N.N.1.1.%.w.%.0.V 3 3 = & xXgXZ A.B.e.r.*.&.&.&.H V N 8 8 3 3 3 4 N V F =.6.*.*.0.e.B.B.pXz.X < # # ; % ^ @X,X>X3X4X3X1X<X>X>X<X<X<X<X<X<X<X<X1X<X<X<X<X<X>X>X<X<X<X<X>X>X>X>X>X>X>X>X2X2X2X2X",
"j j j q 0 $ # - , z ( ) S S ..X. .X.X.X.X.X.B B B S B ..X.X.g.;.m.N.P.1.%.$.r.H 3 3 @ o * . CXmXV C.B.e.e.;.$.6.=.7.F V N 8 8 8 4 N V F &.6.6.*.6.f.e.B.B.mXt.o # , & . - + h _ @X:X1XNX1X<X4X4X<X<X<X<X<X3X<X<X4X4X4X4X1X<X>X>X<X<X<X<X<X<X3X3X>X>X>X>X>X2X2X2X",
"s s d w r $ # . 7 } vXXXS ..X.X.X.X.X.X.X.B B B B B B X.....f.;.m.L.P.-.Z N 3 X o o # # # & CXZXF e.B.e.e.;.;.=.6.6.&.F Z V N 8 N N Z C =.6.*.*.0.f.e.B.hXZX} ; # # & & # # ; u w ! {.+XNX,X,X4X1X<X>X<X4X4X3X>X3X4X4X1X<X<X<X<X<X<X<X3X3X3X3X<X>X*X*X*X-X<X5X5X",
"i r s r s $ . v 8XxXgXy.*...X.X. . .X.X.X. .B B B B X.X. .B y.r.0.>.Z 3 X X . . # # # # > @ CXZXF 0.A.e.0.q.;.=.;.=.O.O.C Z V V F C O.%.6.;.%.;.e.e.B.Z.SXAX| % # # # # # # # # ; t u h _ +XNX+X<X<X<X<X<X<X4X4X4X4X4X4X<X<X<X<X<X<X<X3X3X3X3X<X<X,X,X,X<X5X5X5X",
"i r s s 0 # 2 } yXxX9Xy.g.O.....B ....X.X. .B B B B .X...S F V 3 3 X X X @ & # # # # # & @ CXZXV -.B.C.a.<.;.;.;.*.O.&.F F F H &.O.$.=.8.=.%.0.q.e.B.hXAXAX` # # # # # # # # # ; % t l h u / [.1X1X1X1X1X1X<X<X1X>X>X4X4X<X1X4X<X<X3X3X3X3X3X>X<X<X,X,X<X<X5X5X",
"d r s t % # 7 qXyXgX9Xc.aXr.@...B B ... .B B B B B S B N 4 3 X X o o o . . # - # # # & = X xXHXD F S.V.w.w.;.<.;.=.$.&.F F F =.=.*.*.;.0.;.;.q.e.e.pXAXZXGXn # # # # # # . # # > , i t t y h h ^ T.+X+X+X1X1X<X4X1X4X4X4X>X>X>X<X<X3X3X3X3X3X2X<X<X,X,X<X<X5X5X",
"d r 0 t ; , 7 qXxXgXqX9XgXaX8... . .....S B N 8 8 8 4 3 3 3 . . # # $ ; $ ; ; # # . . & = o tXHXhXV e.V.a.q.<.w.0.6.*.*.&.$.*.8.;.;.;.8.0.;.;.w.b.r.mXHXGXFX= > # # # # # # # # & # # i d i r t h w h {.eXNX1X1X,X,X1X1X1X<X>X4X<X<X3X3X3X3X3X3X<X>X,X,X<X<X5X5X",
"r r y t ; 2 7 9XqXxXqXqXxXcXaXO.S S B N N 4 3 X @ X o o o & % % $ $ $ % % # - . . . o > & o XFXHXt.&.f.a.,.w.<.q.0.=.&.*.*.=.8.;.:.;.0.q.;.;.e.0.pXAXZXPXVXo & # # # # # # # # & & < < , * , a i d y y l / T.+XNX1X1X1X,X,X1X4X<X<X3X3X3X2X2X2X>X>X>X-X5X<X5X5X",
"r s y 0 # o 9 gXgXxXqXqXxXbXcX7.N 8 4 3 X @ . + - . . . . . # # . # > > > + + + . . . & # # | CXHXZXF 0.s.,.<.w.w.;.=.&.=.*.;.8.;.:.;.w.q.;.0.r.r.ZXAXGXGX} = o # # # # # # # # & & & < < < , < , i i i t y h h ^ _ ].+X1XNX1X,X1X<X<X<X3X3X2X>X2X>X-X-X5X5X5X5X",
"s q q 0 , o 9 9XgXgXxXyXxXCXgXD 3 o o @ + + + . - . # - > # , . > . . . @ . . . . . . # # , | IXHXHXt.F e.a.,.<.w.;.=.&.=.%.;.8.;.;.;.q.;.=.=.-.hXAXHXGXGX` o & . # # . . . # # & < < * , < < > < < , , i d s y h l h h ! [ @XNX1X1X1X1X<X<X>X-X3X2X>X-X5X-X5X-X",
"j q s 0 * o 9 9XgXxXxXyXtX X7 o o o . + . . . . & # # - - - - + + . . . o o . . > . # . # # ` IXFXLXZXF =.e.p.<.w.q.=.*.*.o.;.;.;.=.,.q.;.*.F c.ZXJXHXGXFX< & . . . # . . # # # # # * , i i , , < < < p i i i i s d l l y q l ! T.NX1X,X,X1X*X<X>X>X2X2X-X-X5X5X",
"y y r r = o 5 9XqXbXwX7 2 2 . 2 & & & > # # & o & & # + + . . . . . . . o & & & # # , > - # n GXFXHXJXmX&.r.w.p.w.e.=.$.=.C $.$.$.=.%.w.;.C 9.ZXHXHXLXGX7X& $ # . . # . . . # # * , , , i i i i < < < < < p p p a i i d d s s j h ! T.NX1X,X,X*X<X>X2X2X-X-X5X5X",
"0 s r r p & @ qXbXyX7 2 o & > & & = & & & # & # . . . . o o o o . . . # # & * * * * * , # & z FXPXHXLXAXZX6.a.s.e.e.6.*.=.C $.*.$.O.0.0.$.7.AXHXKXPXPXGXD . $ # . . . # # # # # * ; ; ; i i i i , < p p < < < < z < < p a p i r y h L _ @XeX1X,X<X<X3X>X-X-X-X-X",
"i i $ r * & o wXxX` o < . # # # = = & & & & # - . . . . o o o o # # # # $ $ $ * * * * & . & 2 tXFXLXPXLXJXcX6.g.A.e.r.0.%.C *.&.$.&.e.0.H SXHXKXKXLXPXCX2 . # # & # # # . # # - # $ ; ; i t i i i i i i < < < < 7 < < < < < < p d s y h h ^ [.1X:X5X5X5X-X-X-X-X",
"p = * * * & o 0XyXo 7 . $ 1 # > & & & & & > - - . . . . o o o o # # # $ $ $ $ $ $ # * * & & & ` FXGXKXKXLXJXpX6.B.B.b.e.=.$.r.&.&.y.r.H SXLXLXKXKXKXLX X& & & & > # # # # - - - # - ; i i i i i i i i i , , < < < < < < < < p a a x i t y h / NX:X5X5X5X-X-X-X-X",
"v = * i # & = 0X' o . 1 , . & > - - & & & > & - . . . . . . . . # # # # # # # # # # & & , & < < xXFXPXKXKXLXJXc.r.B.A.B.e.0.y.0.r.y.7.AXLXLXKXKXKXKXPXn , > & > > > # # - - > > > , , i i i i i i i i i i , p < p , < , p i , , & & z i t y u [.:X<X5X5X5X-X-X-X",
"{ 7 & , # # = 8Xv o & . - > > . + - & & & & & & # # # # . . . . . o o . . . . + + + + . - # p * t.GXPXKXKXKXJXJXx.e.B.B.x.e.y.e.e.c.AXLXLXKXKXKXKXKXtX# , > > > , ; # # - > > > > > > , , , i i i i i i i i i p i i , i i i t $ 7 o 7 > $ s u _ :X5X5X5X5X-X-X-X",
"yXD & > # # o } o X > > - # o @ + + o o & & & & . # # # # # # . o o o o . . . . + . . . - # , p ` GXGXKXKXLXLXJXAXx.Z.fXMXnXcXcXcX9XAXKXKXKXKXKXKXLX| , , # < & , ; # - - > > > > > > > , , , i i i i i i i i i , i i i * t i i = 2 & > a % y K :X5X5X5X-X-X-X-X",
"xX8X3 o o o 2 n o o > > > > & & > - # & o . & # . . . # # # # . # o & o . . . . . . # # # & , , m IXGXLXKXKXKXKXLXJXHXAXAXZXxXwXD 5 D FXKXKXKXKXLXCX, , 1 > . > > > > > > > , , , , , , , , , , i i i i i i , , , , , , , , , , # > , , > - $ q @X:X5X3X-X>X*X>X",
"xXqXG X o o = z & & o > > & & # & # # . # # # # . . # # # # # . . # # - # # # # # # # # # # & & < tXPXPXLXKXKXKXKX8X` ` n v z < < < & m CXKXKXKXKX} , - > > > > > > > > , , , , , , , i , , , , i i i i i i , , i i i i , , * $ $ ; , , > & # 0 {.:X3X-X>X,X*X<X",
"gXgXt.D X 3 7 & & & o & & & # # & > # # # . . . - - - # # # . . . . # # # # # # # # # # # # # & < } FXPXKXLXKXLX} < . a > & < > & > z > m IXKXLXGXb 1 , > < > > , , , , , , , , , , i i i i , , p i p p p p i i p p i , , * $ $ $ , , , & & - t ! 1X3X*X>X*X>X>X",
"gXgXgXt.3 7 7 & & & o & & & # # # - # # # # # . > > > # . . . # . . # # # # # # . # # # # # # & > ` FXKXKXKXLX{ > & 1 1 < a < & 1 o . . a n GXKX8X< , 1 > < > > , , , , , , , , p p p p i , , , i i i p p p i i i i , , * * * $ $ , * * & & - t h +X*X<X*X*X3X-X",
"hX9XhX9Xv 3 o o o o o & & & & & # # # # # # . . > > # # . . . . . . - - # # # # . # # # # # # & , b CXPXLXKX| # & , , . > , > 1 > 2 < < > & ` GXn , > , > > < > , , , , , , , , a a p p p , , , , i i i i i i i i i , , , , * $ , * * * & # # $ q {.+X<X*X>X3X-X",
"hXpXgX0X' 7 X o o o o & & & & & # # # - > # # . > > # # # # . . . # - # # # . . # # # # # # # # & a R.PXKX| , , , # & & & > & > & & & & . z # ` 1 # 1 > > > 1 > < < , , , , , , p p p p p i , , , i i i i i i i i i i i , , , * * * * * & # # $ q ! 1X:X,X<X-X3X",
"fXpXpXc.t.5 o o o o & # # # & & # . > > > > . . # . . . - # # . . . # # # # # # # # # # # # # & # a ` PX} , # # & . > > . > > # > 2 < & a . , 1 a > z , > , < > , , , , , , , , p p p p i , , , i i i i i i i p i i i , i , , * $ $ * * & # # $ t u [.+X,X<X-X3X",
"hXpXpX9Xt.3 7 . o . # # # # # # # # - > > > . . . . . . - - # . . . # . # # # # . # # # # # > , , # z | & , > . # # > # . # . > & & < > # a , 1 , , , , & 1 > > , , , , , , , , p p p p i , , , , , , , , , , i , * , * , , , , $ $ * & & # # # % u _ +X,X-X3X3X",
"fXpXpX9Xt.9 X o o . . # # # & > # . . . . # # - > # . # # # . . . . . . # # # # # # # # # # # > , , a # - 1 . > 1 > > > < . > m 7 z m , , , , < , , , , , , , > , , , , , , i p p i , i p p , , , , , * * , , i , , , , , , , * $ $ $ # & & & - ; u P +X1X5X3X2X",
"fXpXpX9Xt.5 X o o . # # # # # # # - . . # # > > # . . # # # # . . . . . # # # # # # # # # # # & , , 1 > # # . > # # # 8XtXCXCXCXCXCXiX7Xa > , > , , , , , , , > , > > > , , , , , , , i p p < , , , , * * , , i i , , , , , * * $ $ $ # # # # # # % y [.:X<X3X3X",
"fXdXpX9Xt.3 X o o # # - # # # . # - - - # > > > - # . . . # # # # # . . # # # # # # # # # # # # # ; , , & , 1 & 1 , | CXPXKXKXKXKXKXPXIX| 1 > # > > > , , , , > , , > > & > , , , , , p p p , , , , , * * , , , i , , , * * * $ , , ; - # # # # # % % [ +X1X3X3X",
"fXdXpX9Xt.3 o o o # # # # # # . - - - - # > > > # . . . . # - # # # # # # # # # # & # # # # # # # ; ; , > , 1 # > 1 uXIXKXKXLXKXKXKXPXIX} 1 # , > > > , , , > > , , > > , , , , , , , p p i , , , , , * * * , , , , , * * * * $ , # # # # # # # # # % ^ :X1X2X3X",
"fXdX9X9Xt.X o o o # . . # # # # . . . # # # # # - # . . . # # # # - - - # # # # # # # # # # # # , # # , , > < a < { CXLXKXKXKXKXKXKXPXGX( > , ; , , , , , , , < , , , , , , , , , i p p i , & # , , , * $ $ * , , , * * * * * $ # # # # # $ $ # o . $ P NX1X3X2X",
"fXdXpX9X' X o o # # . . # # # & . . # # # # # # > > # . . # # # - - # - # # # # # # # # # # # # # ; , , a 1 > > n tXFXKXKXKXKXKXKXPXGXtX, , a # , , , , , , , < , , , , , , , , p p p i , * # # , , * * $ $ * , , * * * * * * # # # . # # $ * # o . ; u +X1X3X2X",
"fXpXpX9XG X o o # # # # # # # # . . . . - # # # > > - . # . . . . . # # # # # # # # # # # # # # # , , # , , & m 7XCXLXKXKXKXKXKXKXPXCX` , , 1 , , , 1 1 , , , , , < , , , , i i p p i , * $ & & , , , , * $ * , , , * * * * $ # # # . . # $ * & o . # w [.1X<X3X",
"fXpXpX9XD o o & # # # # # # # # . . # @ > @ - . > > - # # . . . . . # # # # # # # # # # # # # & , # , a a # m 7XCXGXKXKXLXKXKXKXPXCXuX< 1 a , i 1 1 1 1 , > > , 1 1 1 < 1 , p a p p < , $ # & * , , , , , ; , , , , , * & & # # # # . # # $ $ # & & # y {.1X<X3X",
"Z.pXpX9XD @ & . > & # . . # # # . . > . 1 @ . @ > . . > . . . . . 2 & . # # > . > > & # # # # & , # a , a , 7XCXGXKXLXKXKXKXKXLXCXVX( z a 1 , i a 1 , , > > > > 1 - 1 , , 1 , , , & = o , # * = < & , $ , , ; , , - ; & & & # # $ # & # # # $ $ ; # $ t [ NX4X3X",
"aXpXpX9X9 X & # & & & o . . # # > . . 1 . . 1 . @ @ o 2 . o > > . . . & , < . 1 & & & # # # # # # , , , i M R.CXFXKXKXKXKXKXPXIXrX} z < < a , 1 1 1 , < , > > > < > < a 1 , a < < = = = . & = 6 7 & a # - % ; , , # # # & & & - # # # & # # $ $ $ $ $ 0 _ NX4X3X",
"fXZ.9X9X5 X & # # # & o . . # # . o 8X8X8X} } | ` ` n m 2 > o . 2 & > & . < . # # & # # # # # # # 1 # , , a ` iXIXPXLXKXKXLXCX7X} n , , , a , , < < < < < > > , , a < < , , < = 6 6 6 c 6 c 6 6 6 6 , ; t : & & = * & & & & . # # # # & # # $ $ $ ; * 0 ^ NX4X4X",
"fXZ.9X9XX o & # # . o o # . # # 7 . 8XxXKXKXLX7X7X7XrXuXiXxXtXrX8X X{ ` & . 1 > # & # # # # # # # , # , , , a 7XVXHXKXLXLXFXrX{ ` i i a i a i , i < p < < , , , * x p p p < 1 ` D ) ) ) ) ) ) ) ) u.2 + + ; o = & & o > * # # - # # # o # # $ $ $ ; * 0 ! NX3X4X",
"dXZ.9Xz.X o & # . # o o & # # # . z 8XCXLXKXCXt.8X7X7XrXtXiXxXxXiXiXtXtX{ > . . & & > # # # # # & # , a i i , | iXFXLXHXZX7X} } n t i i a $ i p i p p p p , , , d r * < z & o 8X5.5.5.5.5.5.5.5.5.vX7 + ; , = = 2 > @ > $ % . . # # # & # # $ $ # ; # $ l +X<X3X",
"pXpXpXt.X o o # # . o o . # # # . v tXxXIXFXCXrX8XtXuXiXiXxXiXxXiXuXtXuX7X` > & & & & & # # - > , # , , i a , m uXVXZXFXtX X8X8Xi t i i a * p p p p p i i 1 , , ; i x = < < 6 0X5.5.5.5.5.5.5.5.5.vXv @ = = 8 3 3 3 X 2 , % # . # # # & & & ; ; ; - , $ h [.1X4X",
"pXpXpX' o o . # & & o o . . # # < D yXCXCXVXFX7X8XuXVXCXCXCXVXCXCXVXxXiXVX7X` . & & & & # & > > , , , , , i a & 8XCXFXxX8XrXVX` , i p , p p p p i a i i , 1 , , 1 > 6 7 { rXu.u.u.) .Xu.u.u.XXu..X7X' t.u.7.O.@.O.7.9.] ( r o o # # & & > > ; ; ; , < , y {.1X4X",
"pXpX9XD o o . & > o o o . . # # & ` XuXFXFX7Xt.8XrXrXrXtXuXuXiXiXuXyXtXuXtX7Xz & & & & # # > > # , # a i * a < ` VXxXrX8X8X7X= < * < * * p , , i a i 1 , , , > @ 5 A ' yXPXvXu..X} uX X.Xu.vX{ BX} ] cXXXh.+.j.+.h.pX9X' = o = & - & > > > ; ; - # , $ y [ 1X4X",
"dX9X9X9 o > # # . . o o . . # # & n { } } } X7X7X7X7XrXtXtXuXuXtXtXtXtXrXrX7Xz # & # & # # # # > , ; ; , , , < & uXyX} ` 8Xv & < * * * * , , , i , , , < < < 7 4 V O.u.vXPXvXu..X{ VX X7Xt.vX} BXR.9.aXg.h.j.#.#.h.g.pX' z & > & > > > > $ ; ; ; > , $ y / 1X4X",
"dXpX9X9 @ > . # # . o o o o & > . & & # . . < z m n n ` ` { } } 8X7X8X7X7X7X7Xz # # # # # # # # > > , , , * , * < ' 8X` D { & = , , , , , , , , ; ; , < = 6 6 8 Z *.8.8.5.h.5.u. XrX8X X X7Xu.t.8X} ' 8.5.j.#.#.#.#.+.r.A & . > # > > > , ; ; ; ; > > , y ^ NXNX",
"pXpXz.5 @ > . - . . o o o . # # # # . # & & # # & & & # . . # & & & 2 z m n n & # . # # # # # > > & # # $ * * & = 7 ` D ( c & & # - - - , , 1 1 1 1 , 2 6 c A F 0.G.G.D.+.j.5.XXrXVX` 7XiXGX( { uXrXu.5.j.#.#.#.#.#.+.h.c * # > # & > > , , ; ; ; - > i t h NX4X",
"pXpXt.X @ > . > . . . o . . # # . . . # # - # # # # # # # # # & & . o & . . # & # # . # # # # & > & # # $ $ # & & & 7 ( D . < & # - - - - ; 1 1 1 1 , = 6 c D F e.K.A.B.+.+.5.u.rXVX| 7XrXCX' t.uX.Xu.5.j.+.+.+.+.+.+.y.v ; . . # , > > , ; ; ; ; > > i t u NX4X",
"pX9X] X o & . # # . o o o . # # - > > . . . . # . . # # # # # # o o 2 . # < & . # # # # # # # # & & # # # # # # & & > m < > # , - - - - - - , , 1 , > 2 7 c A A F H F D A A A A ( ( a M b ` c v n m c 4 N N 4 4 N 4 N N O . - , ; , > & & $ ; ; ; , , , t u NX1X",
"pX9X' X o & . . . . o o o o . # # + - > > > > # # > > > > > > & > & < . . & . # # # # # # # # # & & & # # # # - > & > & . < # - - - - - > , < 1 , ; - > < 7 6 6 6 7 o 7 6 6 7 z z < i a z < , 1 < < 6 c o 2 @ @ 7 3 3 X = . - - ; , * & * $ ; ; ; , , * t u @X1X",
"pX9XD X o . . . . . . . . . . . > . . . . . # & # & # # . . . . . . > & , . . > # # # # # # # # & # # # # - - , & , > > # - , # - - , * < < p < , ; ; - , > < < p < z z a < = 7 z z a , p x d - , < < z < , , p = < z = = o . # , * * * * $ ; ; ; - , # t u {.NX",
"pXc.D X o . # # . . o o o o # & # . @ > - > & & & & & & & & & & > . > . > # , # # # # . # # # # & . # # # ; ; , , . # > . - , , , , , < z 6 = * # ; : : : , 1 1 i i p p ; a 7 7 < < < z z = i i , 1 < & , p i r r r * * o & & > $ * , , * , ; ; ; # , # i q _ NX"
};

11174
Tests/images/hopper_rgb.xpm Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -10,8 +10,9 @@ import pytest
from PIL import Image, features from PIL import Image, features
from Tests.helper import skip_unless_feature from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"): if sys.platform.startswith("win32") or sys.platform == "ios":
pytest.skip("Fuzzer is linux only", allow_module_level=True) pytest.skip("Fuzzer doesn't run on Windows or iOS", allow_module_level=True)
libjpeg_turbo_version = features.version("libjpeg_turbo") libjpeg_turbo_version = features.version("libjpeg_turbo")
if libjpeg_turbo_version is not None: if libjpeg_turbo_version is not None:
version = packaging.version.parse(libjpeg_turbo_version) version = packaging.version.parse(libjpeg_turbo_version)

View File

@ -188,5 +188,5 @@ class TestEnvVars:
), ),
) )
def test_warnings(self, var: dict[str, str]) -> None: def test_warnings(self, var: dict[str, str]) -> None:
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match=list(var)[0]):
Image._apply_env_variables(var) Image._apply_env_variables(var)

View File

@ -9,9 +9,9 @@ from PIL import _deprecate
"version, expected", "version, expected",
[ [
( (
12, 13,
"Old thing is deprecated and will be removed in Pillow 12 " "Old thing is deprecated and will be removed in Pillow 13 "
r"\(2025-10-15\)\. Use new thing instead\.", r"\(2026-10-15\)\. Use new thing instead\.",
), ),
( (
None, None,
@ -53,18 +53,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
def test_plural() -> None: def test_plural() -> None:
expected = ( 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\." r"Use new thing instead\."
) )
with pytest.warns(DeprecationWarning, match=expected): 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: def test_replacement_and_action() -> None:
expected = "Use only one of 'replacement' and 'action'" expected = "Use only one of 'replacement' and 'action'"
with pytest.raises(ValueError, match=expected): with pytest.raises(ValueError, match=expected):
_deprecate.deprecate( _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: def test_action(action: str) -> None:
expected = ( 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\." r"Upgrade to new thing\."
) )
with pytest.warns(DeprecationWarning, match=expected): 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: def test_no_replacement_or_action() -> None:
expected = ( 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): with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 12) _deprecate.deprecate("Old thing", 13)

View File

@ -19,7 +19,7 @@ def test_check() -> None:
assert features.check_codec(codec) == features.check(codec) assert features.check_codec(codec) == features.check(codec)
for feature in features.features: for feature in features.features:
if "webp" in feature: if "webp" in feature:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="webp"):
assert features.check_feature(feature) == features.check(feature) assert features.check_feature(feature) == features.check(feature)
else: else:
assert features.check_feature(feature) == features.check(feature) assert features.check_feature(feature) == features.check(feature)
@ -49,27 +49,12 @@ def test_version() -> None:
test(codec, features.version_codec) test(codec, features.version_codec)
for feature in features.features: for feature in features.features:
if "webp" in feature: if "webp" in feature:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="webp"):
test(feature, features.version_feature) test(feature, features.version_feature)
else: else:
test(feature, features.version_feature) test(feature, features.version_feature)
def test_webp_transparency() -> None:
with pytest.warns(DeprecationWarning):
assert (features.check("transp_webp") or False) == features.check_module("webp")
def test_webp_mux() -> None:
with pytest.warns(DeprecationWarning):
assert (features.check("webp_mux") or False) == features.check_module("webp")
def test_webp_anim() -> None:
with pytest.warns(DeprecationWarning):
assert (features.check("webp_anim") or False) == features.check_module("webp")
@skip_unless_feature("libjpeg_turbo") @skip_unless_feature("libjpeg_turbo")
def test_libjpeg_turbo_version() -> None: def test_libjpeg_turbo_version() -> None:
version = features.version("libjpeg_turbo") version = features.version("libjpeg_turbo")
@ -95,10 +80,9 @@ def test_check_codecs(feature: str) -> None:
def test_check_warns_on_nonexistent() -> None: def test_check_warns_on_nonexistent() -> None:
with pytest.warns(UserWarning) as cm: with pytest.warns(UserWarning, match="Unknown feature 'typo'."):
has_feature = features.check("typo") has_feature = features.check("typo")
assert has_feature is False assert has_feature is False
assert str(cm[-1].message) == "Unknown feature 'typo'."
def test_supported_modules() -> None: def test_supported_modules() -> None:

View File

@ -303,11 +303,11 @@ def test_apng_chunk_errors() -> None:
assert isinstance(im, PngImagePlugin.PngImageFile) assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="Invalid APNG"):
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im = Image.open("Tests/images/apng/chunk_multi_actl.png")
im.load() assert isinstance(im, PngImagePlugin.PngImageFile)
assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated
assert not im.is_animated im.close()
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile) assert isinstance(im, PngImagePlugin.PngImageFile)
@ -330,18 +330,20 @@ def test_apng_chunk_errors() -> None:
def test_apng_syntax_errors() -> None: def test_apng_syntax_errors() -> None:
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="Invalid APNG"):
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: im = Image.open("Tests/images/apng/syntax_num_frames_zero.png")
assert isinstance(im, PngImagePlugin.PngImageFile) assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() im.load()
im.close()
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="Invalid APNG"):
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: im = Image.open("Tests/images/apng/syntax_num_frames_zero_default.png")
assert isinstance(im, PngImagePlugin.PngImageFile) assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
im.load() im.load()
im.close()
# we can handle this case gracefully # we can handle this case gracefully
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
@ -354,11 +356,12 @@ def test_apng_syntax_errors() -> None:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() im.load()
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="Invalid APNG"):
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: im = Image.open("Tests/images/apng/syntax_num_frames_invalid.png")
assert isinstance(im, PngImagePlugin.PngImageFile) assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
im.load() im.load()
im.close()
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -77,8 +77,8 @@ class TestUnsupportedAvif:
def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
with pytest.warns(UserWarning): with pytest.raises(UnidentifiedImageError):
with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning, match="AVIF support not installed"):
with Image.open(TEST_AVIF_FILE): with Image.open(TEST_AVIF_FILE):
pass pass
@ -254,7 +254,9 @@ class TestFileAvif:
assert_image(im, "RGBA", (64, 64)) assert_image(im, "RGBA", (64, 64))
# image has 876 transparent pixels # image has 876 transparent pixels
assert im.getchannel("A").getcolors()[0] == (876, 0) colors = im.getchannel("A").getcolors()
assert colors is not None
assert colors[0] == (876, 0)
def test_save_transparent(self, tmp_path: Path) -> None: def test_save_transparent(self, tmp_path: Path) -> None:
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))

View File

@ -511,3 +511,20 @@ def test_save_dx10_bc5(tmp_path: Path) -> None:
im = hopper("L") im = hopper("L")
with pytest.raises(OSError, match="only RGB mode can be written as BC5"): with pytest.raises(OSError, match="only RGB mode can be written as BC5"):
im.save(out, pixel_format="BC5") im.save(out, pixel_format="BC5")
@pytest.mark.parametrize(
"pixel_format, mode",
(
("DXT1", "RGBA"),
("DXT3", "RGBA"),
("DXT5", "RGBA"),
("BC2", "RGBA"),
("BC3", "RGBA"),
("BC5", "RGB"),
),
)
def test_save_large_file(tmp_path: Path, pixel_format: str, mode: str) -> None:
im = hopper(mode).resize((440, 440))
# should not error in valgrind
im.save(tmp_path / "img.dds", pixel_format=pixel_format)

View File

@ -100,6 +100,18 @@ def test_l_mode_after_rgb() -> None:
assert im.mode == "RGB" 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: def test_palette_not_needed_for_second_frame() -> None:
with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im:
im.seek(1) im.seek(1)
@ -224,6 +236,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None:
out = BytesIO() out = BytesIO()
im.save(out, "GIF", optimize=optimize) im.save(out, "GIF", optimize=optimize)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.palette is not None
assert len(reloaded.palette.palette) // 3 == colors assert len(reloaded.palette.palette) // 3 == colors
@ -540,7 +553,9 @@ def test_dispose_background_transparency() -> None:
img.seek(2) img.seek(2)
px = img.load() px = img.load()
assert px is not None assert px is not None
assert px[35, 30][3] == 0 value = px[35, 30]
assert isinstance(value, tuple)
assert value[3] == 0
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1229,7 +1244,9 @@ def test_removed_transparency(tmp_path: Path) -> None:
im.putpixel((x, 0), (x, 0, 0)) im.putpixel((x, 0), (x, 0, 0))
im.info["transparency"] = (255, 255, 255) im.info["transparency"] = (255, 255, 255)
with pytest.warns(UserWarning): with pytest.warns(
UserWarning, match="Couldn't allocate palette entry for transparency"
):
im.save(out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1251,7 +1268,7 @@ def test_rgb_transparency(tmp_path: Path) -> None:
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
im.info["transparency"] = b"" im.info["transparency"] = b""
ims = [Image.new("RGB", (1, 1))] ims = [Image.new("RGB", (1, 1))]
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="should be converted to RGBA images"):
im.save(out, save_all=True, append_images=ims) im.save(out, save_all=True, append_images=ims)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1359,6 +1376,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
# Assert that the frames are correct, and each frame has the same palette # Assert that the frames are correct, and each frame has the same palette
assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
assert im.palette is not None assert im.palette is not None
assert im.global_palette is not None
assert im.palette.palette == im.global_palette.palette assert im.palette.palette == im.global_palette.palette
im.seek(1) im.seek(1)
@ -1422,7 +1440,9 @@ def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None:
def test_lzw_bits() -> None: def test_lzw_bits() -> None:
# see https://github.com/python-pillow/Pillow/issues/2811 # see https://github.com/python-pillow/Pillow/issues/2811
with Image.open("Tests/images/issue_2811.gif") as im: with Image.open("Tests/images/issue_2811.gif") as im:
assert im.tile[0][3][0] == 11 # LZW bits args = im.tile[0][3]
assert isinstance(args, tuple)
assert args[0] == 11 # LZW bits
# codec error prepatch # codec error prepatch
im.load() im.load()
@ -1477,7 +1497,11 @@ def test_saving_rgba(tmp_path: Path) -> None:
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
reloaded_rgba = reloaded.convert("RGBA") reloaded_rgba = reloaded.convert("RGBA")
assert reloaded_rgba.load()[0, 0][3] == 0 px = reloaded_rgba.load()
assert px is not None
value = px[0, 0]
assert isinstance(value, tuple)
assert value[3] == 0
@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False}))

View File

@ -93,19 +93,11 @@ def test_sizes() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert isinstance(im, IcnsImagePlugin.IcnsImageFile) assert isinstance(im, IcnsImagePlugin.IcnsImageFile)
for w, h, r in im.info["sizes"]: for w, h, r in im.info["sizes"]:
wr = w * r
hr = h * r
with pytest.warns(DeprecationWarning):
im.size = (w, h, r)
im.load()
assert im.mode == "RGBA"
assert im.size == (wr, hr)
# Test using load() with scale # Test using load() with scale
im.size = (w, h) im.size = (w, h)
im.load(scale=r) im.load(scale=r)
assert im.mode == "RGBA" assert im.mode == "RGBA"
assert im.size == (wr, hr) assert im.size == (w * r, h * r)
# Check that we cannot load an incorrect size # Check that we cannot load an incorrect size
with pytest.raises(ValueError): with pytest.raises(ValueError):

View File

@ -99,6 +99,7 @@ def test_getpixel(tmp_path: Path) -> None:
reloaded.load() reloaded.load()
reloaded.size = (32, 32) reloaded.size = (32, 32)
assert reloaded.load() is not None
assert reloaded.getpixel((0, 0)) == (18, 20, 62) assert reloaded.getpixel((0, 0)) == (18, 20, 62)
@ -233,7 +234,7 @@ def test_save_append_images(tmp_path: Path) -> None:
def test_unexpected_size() -> None: def test_unexpected_size() -> None:
# This image has been manually hexedited to state that it is 16x32 # This image has been manually hexedited to state that it is 16x32
# while the image within is still 16x16 # while the image within is still 16x16
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="Image was not the expected size"):
with Image.open("Tests/images/hopper_unexpected.ico") as im: with Image.open("Tests/images/hopper_unexpected.ico") as im:
assert im.size == (16, 16) assert im.size == (16, 16)

View File

@ -1,11 +1,8 @@
from __future__ import annotations from __future__ import annotations
import sys from io import BytesIO
from io import BytesIO, StringIO
import pytest from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
from PIL import Image, IptcImagePlugin
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
@ -23,6 +20,9 @@ def test_open() -> None:
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
assert_image_equal(im, expected) assert_image_equal(im, expected)
with Image.open(f) as im:
assert im.load() is not None
def test_getiptcinfo_jpg_none() -> None: def test_getiptcinfo_jpg_none() -> None:
# Arrange # Arrange
@ -75,13 +75,19 @@ def test_getiptcinfo_zero_padding() -> None:
def test_getiptcinfo_tiff() -> 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: with Image.open("Tests/images/hopper.Lab.tif") as im:
# Act
iptc = IptcImagePlugin.getiptcinfo(im) iptc = IptcImagePlugin.getiptcinfo(im)
# Assert assert iptc == expected
assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"}
# 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: def test_getiptcinfo_tiff_none() -> None:
@ -92,35 +98,3 @@ def test_getiptcinfo_tiff_none() -> None:
# Assert # Assert
assert iptc is None assert iptc is None
def test_i() -> None:
# Arrange
c = b"a"
# Act
with pytest.warns(DeprecationWarning):
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):
IptcImagePlugin.dump(c)
# Assert
assert mystdout.getvalue() == "61 62 63 \n"
def test_pad_deprecation() -> None:
with pytest.warns(DeprecationWarning):
assert IptcImagePlugin.PAD == b"\0\0\0\0"

View File

@ -130,21 +130,7 @@ class TestFileJpeg:
def test_cmyk(self) -> None: def test_cmyk(self) -> None:
# Test CMYK handling. Thanks to Tim and Charlie for test data, # Test CMYK handling. Thanks to Tim and Charlie for test data,
# Michael for getting me to look one more time. # Michael for getting me to look one more time.
f = "Tests/images/pil_sample_cmyk.jpg" def check(im: ImageFile.ImageFile) -> None:
with Image.open(f) as im:
# the source image has red pixels in the upper left corner.
c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
assert c == 0.0
assert m > 0.8
assert y > 0.8
assert k == 0.0
# the opposite corner is black
c, m, y, k = (
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
)
assert k > 0.9
# roundtrip, and check again
im = self.roundtrip(im)
cmyk = im.getpixel((0, 0)) cmyk = im.getpixel((0, 0))
assert isinstance(cmyk, tuple) assert isinstance(cmyk, tuple)
c, m, y, k = (x / 255.0 for x in cmyk) c, m, y, k = (x / 255.0 for x in cmyk)
@ -152,11 +138,19 @@ class TestFileJpeg:
assert m > 0.8 assert m > 0.8
assert y > 0.8 assert y > 0.8
assert k == 0.0 assert k == 0.0
# the opposite corner is black
cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1)) cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1))
assert isinstance(cmyk, tuple) assert isinstance(cmyk, tuple)
k = cmyk[3] / 255.0 k = cmyk[3] / 255.0
assert k > 0.9 assert k > 0.9
with Image.open("Tests/images/pil_sample_cmyk.jpg") as im:
# the source image has red pixels in the upper left corner.
check(im)
# roundtrip, and check again
check(self.roundtrip(im))
def test_rgb(self) -> None: def test_rgb(self) -> None:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]: def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]:
return tuple(v[0] for v in im.layer) return tuple(v[0] for v in im.layer)
@ -617,6 +611,24 @@ class TestFileJpeg:
None 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 # list of qtable lists
assert_image_similar( assert_image_similar(
im, im,
@ -752,10 +764,13 @@ class TestFileJpeg:
# Act # Act
# Shouldn't raise error # Shouldn't raise error
fn = "Tests/images/sugarshack_bad_mpo_header.jpg" with pytest.warns(UserWarning, match="malformed MPO file"):
with pytest.warns(UserWarning, Image.open, fn) as im: im = Image.open("Tests/images/sugarshack_bad_mpo_header.jpg")
# Assert
assert im.format == "JPEG" # Assert
assert im.format == "JPEG"
im.close()
@pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr"))
def test_save_correct_modes(self, mode: str) -> None: def test_save_correct_modes(self, mode: str) -> None:
@ -1067,10 +1082,16 @@ class TestFileJpeg:
for marker in b"\xff\xd8", b"\xff\xd9": for marker in b"\xff\xd8", b"\xff\xd9":
assert marker in data[1] assert marker in data[1]
assert marker in data[2] assert marker in data[2]
# DHT, DQT
for marker in b"\xff\xc4", b"\xff\xdb": # DQT
markers = [b"\xff\xdb"]
if features.check_feature("libjpeg_turbo"):
# DHT
markers.append(b"\xff\xc4")
for marker in markers:
assert marker in data[1] assert marker in data[1]
assert marker not in data[2] assert marker not in data[2]
# SOF0, SOS, APP0 (JFIF header) # SOF0, SOS, APP0 (JFIF header)
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
assert marker not in data[1] assert marker not in data[1]
@ -1094,14 +1115,6 @@ class TestFileJpeg:
assert im._repr_jpeg_() is None 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):
assert im.huffman_ac == {}
with pytest.warns(DeprecationWarning):
assert im.huffman_dc == {}
@pytest.mark.skipif(not is_win32(), reason="Windows only") @pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg") @skip_unless_feature("jpg")

View File

@ -256,19 +256,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, tiffinfo=new_ifd) im.save(out, tiffinfo=new_ifd)
@pytest.mark.parametrize( @pytest.mark.parametrize("libtiff", (True, False))
"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,
),
)
def test_custom_metadata( def test_custom_metadata(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None: ) -> None:
@ -724,8 +712,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) 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( def test_crashing_metadata(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
@ -777,19 +764,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert icc_libtiff is not None assert icc_libtiff is not None
assert icc == icc_libtiff assert icc == icc_libtiff
@pytest.mark.parametrize( @pytest.mark.parametrize("libtiff", (True, False))
"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,
),
)
def test_write_icc( def test_write_icc(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None: ) -> None:

View File

@ -27,6 +27,6 @@ def test_valid_file() -> None:
# Assert # Assert
assert im.format == "MCIDAS" assert im.format == "MCIDAS"
assert im.mode == "I" assert im.mode == "I;16B"
assert im.size == (1800, 400) assert im.size == (1800, 400)
assert_image_equal_tofile(im, saved_file) assert_image_equal_tofile(im, saved_file)

View File

@ -156,6 +156,7 @@ def test_reload_exif_after_seek() -> None:
def test_mp(test_file: str) -> None: def test_mp(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo is not None
assert mpinfo[45056] == b"0100" assert mpinfo[45056] == b"0100"
assert mpinfo[45057] == 2 assert mpinfo[45057] == 2
@ -165,6 +166,7 @@ def test_mp_offset() -> None:
# in APP2 data, in contrast to normal 8 # in APP2 data, in contrast to normal 8
with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo is not None
assert mpinfo[45056] == b"0100" assert mpinfo[45056] == b"0100"
assert mpinfo[45057] == 2 assert mpinfo[45057] == 2
@ -181,6 +183,7 @@ def test_mp_no_data() -> None:
def test_mp_attribute(test_file: str) -> None: def test_mp_attribute(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo is not None
for frame_number, mpentry in enumerate(mpinfo[0xB002]): for frame_number, mpentry in enumerate(mpinfo[0xB002]):
mpattr = mpentry["Attribute"] mpattr = mpentry["Attribute"]
if frame_number: if frame_number:
@ -293,16 +296,18 @@ def test_save_all() -> None:
assert_image_similar(im, im_reloaded, 30) assert_image_similar(im, im_reloaded, 30)
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00") for colors in (("#f00",), ("#f00", "#0f0")):
im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) append_images = [Image.new("RGB", (1, 1), color) for color in colors]
im_reloaded = roundtrip(im, save_all=True, append_images=append_images)
assert_image_equal(im, im_reloaded) assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile) assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100" assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1) for im_expected in append_images:
assert_image_similar(im2, im_reloaded, 1) im_reloaded.seek(im_reloaded.tell() + 1)
assert_image_similar(im_reloaded, im_expected, 1)
# Test that a single frame image will not be saved as an MPO # Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im, save_all=True) jpg = roundtrip(im, save_all=True)
@ -312,10 +317,24 @@ def test_save_all() -> None:
def test_save_xmp() -> None: def test_save_xmp() -> None:
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00") 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"} 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"]
assert im_reloaded.info["xmp"] == b"First frame" # Test that encoderinfo is unchanged
assert im2.encoderinfo == {"xmp": b"Second frame"}
im_reloaded.seek(1)
assert im_reloaded.info["xmp"] == b"Second frame"

View File

@ -805,7 +805,7 @@ class TestFilePng:
test_file = tmp_path / "out.png" test_file = tmp_path / "out.png"
im = hopper("I") im = hopper("I")
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="Saving I mode images as PNG"):
im.save(test_file) im.save(test_file)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -71,6 +72,15 @@ def test_invalid_file() -> None:
SgiImagePlugin.SgiImageFile(invalid_file) SgiImagePlugin.SgiImageFile(invalid_file)
def test_unsupported_image_mode() -> None:
with open("Tests/images/hopper.rgb", "rb") as fp:
data = fp.read()
data = data[:3] + b"\x03" + data[4:]
with pytest.raises(ValueError, match="Unsupported SGI image mode"):
with Image.open(BytesIO(data)):
pass
def roundtrip(img: Image.Image, tmp_path: Path) -> None: def roundtrip(img: Image.Image, tmp_path: Path) -> None:
out = tmp_path / "temp.sgi" out = tmp_path / "temp.sgi"
img.save(out, format="sgi") img.save(out, format="sgi")
@ -109,3 +119,11 @@ def test_unsupported_mode(tmp_path: Path) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(out, format="sgi") im.save(out, format="sgi")
def test_unsupported_number_of_bytes_per_pixel(tmp_path: Path) -> None:
im = hopper()
out = tmp_path / "temp.sgi"
with pytest.raises(ValueError, match="Unsupported number of bytes per pixel"):
im.save(out, bpc=3)

View File

@ -190,7 +190,9 @@ def test_save_id_section(tmp_path: Path) -> None:
# Save with custom id section greater than 255 characters # Save with custom id section greater than 255 characters
id_section = b"Test content" * 25 id_section = b"Test content" * 25
with pytest.warns(UserWarning): with pytest.warns(
UserWarning, match="id_section has been trimmed to 255 characters"
):
im.save(out, id_section=id_section) im.save(out, id_section=id_section)
with Image.open(out) as test_im: with Image.open(out) as test_im:
@ -220,12 +222,16 @@ def test_horizontal_orientations() -> None:
with Image.open("Tests/images/rgb32rle_top_right.tga") as im: with Image.open("Tests/images/rgb32rle_top_right.tga") as im:
px = im.load() px = im.load()
assert px is not None assert px is not None
assert px[90, 90][:3] == (0, 0, 0) value = px[90, 90]
assert isinstance(value, tuple)
assert value[:3] == (0, 0, 0)
with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im:
px = im.load() px = im.load()
assert px is not None assert px is not None
assert px[90, 90][:3] == (0, 255, 0) value = px[90, 90]
assert isinstance(value, tuple)
assert value[:3] == (0, 255, 0)
def test_save_rle(tmp_path: Path) -> None: def test_save_rle(tmp_path: Path) -> None:

View File

@ -49,25 +49,10 @@ class TestFileTiff:
assert im.size == (128, 128) assert im.size == (128, 128)
assert im.format == "TIFF" assert im.format == "TIFF"
hopper("1").save(filename) for mode in ("1", "L", "P", "RGB", "I", "I;16", "I;16L"):
with Image.open(filename): hopper(mode).save(filename)
pass with Image.open(filename):
pass
hopper("L").save(filename)
with Image.open(filename):
pass
hopper("P").save(filename)
with Image.open(filename):
pass
hopper("RGB").save(filename)
with Image.open(filename):
pass
hopper("I").save(filename)
with Image.open(filename):
pass
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(self) -> None: def test_unclosed_file(self) -> None:
@ -236,7 +221,7 @@ class TestFileTiff:
assert isinstance(im, JpegImagePlugin.JpegImageFile) assert isinstance(im, JpegImagePlugin.JpegImageFile)
# Should not raise struct.error. # Should not raise struct.error.
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="Corrupt EXIF data"):
im._getexif() im._getexif()
def test_save_rgba(self, tmp_path: Path) -> None: def test_save_rgba(self, tmp_path: Path) -> None:
@ -696,16 +681,21 @@ class TestFileTiff:
assert im.tag_v2[278] == 256 assert im.tag_v2[278] == 256
im = hopper() im = hopper()
im.encoderinfo = {"tiffinfo": {278: 100}}
im2 = Image.new("L", (128, 128)) im2 = Image.new("L", (128, 128))
im2.encoderinfo = {"tiffinfo": {278: 256}} im3 = im2.copy()
im.save(outfile, save_all=True, append_images=[im2]) im3.encoderinfo = {"tiffinfo": {278: 300}}
im.save(outfile, save_all=True, tiffinfo={278: 200}, append_images=[im2, im3])
with Image.open(outfile) as im: with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile) assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[278] == 128 assert im.tag_v2[278] == 100
im.seek(1) 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: def test_strip_raw(self) -> None:
infile = "Tests/images/tiff_strip_raw.tif" infile = "Tests/images/tiff_strip_raw.tif"
@ -1029,7 +1019,7 @@ class TestFileTiff:
@timeout_unless_slower_valgrind(2) @timeout_unless_slower_valgrind(2)
def test_oom(self, test_file: str) -> None: def test_oom(self, test_file: str) -> None:
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="Corrupt EXIF data"):
with Image.open(test_file): with Image.open(test_file):
pass pass

View File

@ -300,7 +300,7 @@ def test_empty_metadata() -> None:
head = f.read(8) head = f.read(8)
info = TiffImagePlugin.ImageFileDirectory(head) info = TiffImagePlugin.ImageFileDirectory(head)
# Should not raise struct.error. # Should not raise struct.error.
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="Corrupt EXIF data"):
info.load(f) info.load(f)
@ -481,7 +481,7 @@ def test_too_many_entries() -> None:
ifd.tagtype[277] = TiffTags.SHORT ifd.tagtype[277] = TiffTags.SHORT
# Should not raise ValueError. # Should not raise ValueError.
with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="Metadata Warning"):
assert ifd[277] == 4 assert ifd[277] == 4

View File

@ -33,8 +33,8 @@ class TestUnsupportedWebp:
monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False) monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False)
file_path = "Tests/images/hopper.webp" file_path = "Tests/images/hopper.webp"
with pytest.warns(UserWarning): with pytest.raises(OSError):
with pytest.raises(OSError): with pytest.warns(UserWarning, match="WEBP support not installed"):
with Image.open(file_path): with Image.open(file_path):
pass pass
@ -219,6 +219,7 @@ class TestFileWebp:
# Save P mode GIF with background # Save P mode GIF with background
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1)) original_value = im.convert("RGB").getpixel((1, 1))
assert isinstance(original_value, tuple)
# Save as WEBP # Save as WEBP
im.save(out_webp, save_all=True) im.save(out_webp, save_all=True)
@ -230,6 +231,7 @@ class TestFileWebp:
with Image.open(out_gif) as reread: with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1)) reread_value = reread.convert("RGB").getpixel((1, 1))
assert isinstance(reread_value, tuple)
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3)) difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3))
assert difference < 5 assert difference < 5

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
import pytest import pytest
from PIL import Image, XpmImagePlugin from PIL import Image, XpmImagePlugin
@ -17,7 +19,45 @@ def test_sanity() -> None:
assert im.format == "XPM" assert im.format == "XPM"
# large error due to quantization->44 colors. # large error due to quantization->44 colors.
assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) assert_image_similar(im.convert("RGB"), hopper(), 23)
def test_bpp2() -> None:
with Image.open("Tests/images/hopper_bpp2.xpm") as im:
assert_image_similar(im.convert("RGB"), hopper(), 11)
def test_rgb() -> None:
with Image.open("Tests/images/hopper_rgb.xpm") as im:
assert im.mode == "RGB"
assert_image_similar(im, hopper(), 16)
def test_truncated_header() -> None:
data = b"/* XPM */"
with pytest.raises(SyntaxError, match="broken XPM file"):
with XpmImagePlugin.XpmImageFile(BytesIO(data)):
pass
def test_cannot_read_color() -> None:
with open(TEST_FILE, "rb") as fp:
data = fp.read().split(b"#")[0]
with pytest.raises(ValueError, match="cannot read this XPM file"):
with Image.open(BytesIO(data)):
pass
with pytest.raises(ValueError, match="cannot read this XPM file"):
with Image.open(BytesIO(data + b"invalid")):
pass
def test_not_enough_image_data() -> None:
with open(TEST_FILE, "rb") as fp:
data = fp.read().split(b"/* pixels */")[0]
with Image.open(BytesIO(data)) as im:
with pytest.raises(ValueError, match="not enough image data"):
im.load()
def test_invalid_file() -> None: def test_invalid_file() -> None:

View File

@ -30,7 +30,6 @@ from .helper import (
assert_image_similar_tofile, assert_image_similar_tofile,
assert_not_all_same, assert_not_all_same,
hopper, hopper,
is_big_endian,
is_win32, is_win32,
mark_if_feature_version, mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
@ -50,19 +49,10 @@ except ImportError:
PrettyPrinter = None PrettyPrinter = None
# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
return Image.new(mode, size)
else:
return Image.new(mode, size)
class TestImage: 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: 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")) @pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None: def test_image_modes_fail(self, mode: str) -> None:
@ -141,8 +131,8 @@ class TestImage:
monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True) monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True)
im = io.BytesIO(b"") im = io.BytesIO(b"")
with pytest.warns(UserWarning): with pytest.raises(UnidentifiedImageError):
with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning, match="opening failed"):
with Image.open(im): with Image.open(im):
pass pass
@ -160,6 +150,10 @@ class TestImage:
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
im.mode = "P" # type: ignore[misc] im.mode = "P" # type: ignore[misc]
def test_empty_path(self) -> None:
with pytest.raises(FileNotFoundError):
Image.open("")
def test_invalid_image(self) -> None: def test_invalid_image(self) -> None:
im = io.BytesIO(b"") im = io.BytesIO(b"")
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
@ -1008,7 +1002,7 @@ class TestImage:
def test_get_child_images(self) -> None: def test_get_child_images(self) -> None:
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"):
assert im.get_child_images() == [] assert im.get_child_images() == []
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
@ -1138,39 +1132,29 @@ class TestImage:
assert len(caplog.records) == 0 assert len(caplog.records) == 0
assert im.fp is None assert im.fp is None
def test_deprecation(self) -> None:
with pytest.warns(DeprecationWarning):
assert not Image.isImageType(None)
class TestImageBytes: 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: def test_roundtrip_bytes_constructor(self, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
source_bytes = im.tobytes() source_bytes = im.tobytes()
if mode.startswith("BGR;"): reloaded = Image.frombytes(mode, im.size, source_bytes)
with pytest.warns(DeprecationWarning):
reloaded = Image.frombytes(mode, im.size, source_bytes)
else:
reloaded = Image.frombytes(mode, im.size, source_bytes)
assert reloaded.tobytes() == 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: def test_roundtrip_bytes_method(self, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
source_bytes = im.tobytes() source_bytes = im.tobytes()
reloaded = helper_image_new(mode, im.size) reloaded = Image.new(mode, im.size)
reloaded.frombytes(source_bytes) reloaded.frombytes(source_bytes)
assert reloaded.tobytes() == 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: 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) im = hopper(mode)
reloaded = helper_image_new(mode, im.size) reloaded = Image.new(mode, im.size)
reloaded.putdata(im.getdata()) reloaded.putdata(im.getdata())
assert_image_equal(im, reloaded) assert_image_equal(im, reloaded)

View File

@ -123,10 +123,6 @@ class TestImageGetPixel:
bands = Image.getmodebands(mode) bands = Image.getmodebands(mode)
if bands == 1: if bands == 1:
return 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)) return tuple(range(1, bands + 1))
def check(self, mode: str, expected_color_int: int | None = None) -> None: 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: def test_basic(self, mode: str) -> None:
self.check(mode) self.check(mode)
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
def test_deprecated(self, mode: str) -> None:
with pytest.warns(DeprecationWarning):
self.check(mode)
def test_list(self) -> None: def test_list(self) -> None:
im = hopper() im = hopper()
assert im.getpixel([0, 0]) == (20, 20, 70) assert im.getpixel([0, 0]) == (20, 20, 70)
@ -218,7 +209,7 @@ class TestImageGetPixel:
class TestImagePutPixelError: class TestImagePutPixelError:
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] IMAGE_MODES1 = ["LA", "RGB", "RGBA"]
IMAGE_MODES2 = ["L", "I", "I;16"] IMAGE_MODES2 = ["L", "I", "I;16"]
INVALID_TYPES = ["foo", 1.0, None] INVALID_TYPES = ["foo", 1.0, None]
@ -234,11 +225,6 @@ class TestImagePutPixelError:
( (
("L", (0, 2), "color must be int or single-element tuple"), ("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"), ("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", "RGB",
(0, 2, 5), (0, 2, 5),

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import Any
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -13,6 +13,7 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
im = hopper().resize((128, 100)) im = hopper().resize((128, 100))
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
import numpy.typing as npt import numpy.typing as npt
@ -47,7 +48,7 @@ def test_toarray() -> None:
with pytest.raises(OSError): with pytest.raises(OSError):
numpy.array(im_truncated) numpy.array(im_truncated)
else: else:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="__array_interface__"):
numpy.array(im_truncated) numpy.array(im_truncated)
@ -101,7 +102,8 @@ def test_fromarray_strides_without_tobytes() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)})
Image.fromarray(wrapped, "L") with pytest.warns(DeprecationWarning, match="'mode' parameter"):
Image.fromarray(wrapped, "L")
def test_fromarray_palette() -> None: def test_fromarray_palette() -> None:
@ -110,7 +112,8 @@ def test_fromarray_palette() -> None:
a = numpy.array(i) a = numpy.array(i)
# Act # Act
out = Image.fromarray(a, "P") with pytest.warns(DeprecationWarning, match="'mode' parameter"):
out = Image.fromarray(a, "P")
# Assert that the Python and C palettes match # Assert that the Python and C palettes match
assert out.palette is not None assert out.palette is not None

View File

@ -203,7 +203,10 @@ def test_trns_RGB(tmp_path: Path) -> None:
assert "transparency" not in im_rgba.info assert "transparency" not in im_rgba.info
assert im_rgba.getpixel((0, 0)) == (0, 0, 0, 0) assert im_rgba.getpixel((0, 0)) == (0, 0, 0, 0)
im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE) with pytest.warns(
UserWarning, match="Couldn't allocate palette entry for transparency"
):
im_p = im.convert("P", palette=Image.Palette.ADAPTIVE)
assert "transparency" not in im_p.info assert "transparency" not in im_p.info
im_p.save(f) im_p.save(f)

View File

@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest
from .helper import hopper from .helper import hopper
@ -10,10 +8,3 @@ def test_sanity() -> None:
type_repr = repr(type(im.getim())) type_repr = repr(type(im.getim()))
assert "PyCapsule" in type_repr assert "PyCapsule" in type_repr
with pytest.warns(DeprecationWarning):
assert isinstance(im.im.id, int)
with pytest.warns(DeprecationWarning):
ptrs = dict(im.im.unsafe_ptrs)
assert ptrs.keys() == {"image8", "image32", "image"}

View File

@ -78,16 +78,6 @@ def test_mode_F() -> None:
assert list(im.getdata()) == target 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):
im = Image.new(mode, (1, 2))
im.putdata(data)
assert list(im.getdata()) == data
def test_array_B() -> None: def test_array_B() -> None:
# shouldn't segfault # shouldn't segfault
# see https://github.com/python-pillow/Pillow/issues/1008 # see https://github.com/python-pillow/Pillow/issues/1008

View File

@ -70,6 +70,7 @@ def test_quantize_no_dither() -> None:
converted = image.quantize(dither=Image.Dither.NONE, palette=palette) converted = image.quantize(dither=Image.Dither.NONE, palette=palette)
assert converted.mode == "P" assert converted.mode == "P"
assert converted.palette is not None assert converted.palette is not None
assert palette.palette is not None
assert converted.palette.palette == palette.palette.palette assert converted.palette.palette == palette.palette.palette

View File

@ -324,7 +324,7 @@ class TestImageResize:
im = hopper(mode) im = hopper(mode)
assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) 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: def test_default_filter_nearest(self, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))

View File

@ -48,6 +48,7 @@ class TestImageTransform:
im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0]
) )
assert im.palette is not None assert im.palette is not None
assert transformed.palette is not None
assert im.palette.palette == transformed.palette.palette assert im.palette.palette == transformed.palette.palette
def test_extent(self) -> None: def test_extent(self) -> None:

View File

@ -54,10 +54,6 @@ def skip_missing() -> None:
def test_sanity() -> None: def test_sanity() -> None:
# basic smoke test. # basic smoke test.
# this mostly follows the cms_test outline. # this mostly follows the cms_test outline.
with pytest.warns(DeprecationWarning):
v = ImageCms.versions() # should return four strings
assert v[0] == "1.0.0 pil"
assert list(map(type, v)) == [str, str, str, str]
# internal version number # internal version number
version = features.version_module("littlecms2") 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) 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):
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
def test_rgb_lab(mode: str) -> None: def test_rgb_lab(mode: str) -> None:
im = Image.new(mode, (1, 1)) im = Image.new(mode, (1, 1))
@ -703,22 +693,14 @@ def test_cmyk_lab() -> None:
def test_deprecation() -> None: def test_deprecation() -> None:
with pytest.warns(DeprecationWarning):
assert ImageCms.DESCRIPTION.strip().startswith("pyCMS")
with pytest.warns(DeprecationWarning):
assert ImageCms.VERSION == "1.0.0 pil"
with pytest.warns(DeprecationWarning):
assert isinstance(ImageCms.FLAGS, dict)
profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) profile = ImageCmsProfile(ImageCms.createProfile("sRGB"))
with pytest.warns(DeprecationWarning): with pytest.warns(
ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB") DeprecationWarning, match="ImageCms.ImageCmsProfile.product_name"
with pytest.warns(DeprecationWarning): ):
ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B")
with pytest.warns(DeprecationWarning):
profile.product_name profile.product_name
with pytest.warns(DeprecationWarning): with pytest.warns(
DeprecationWarning, match="ImageCms.ImageCmsProfile.product_info"
):
profile.product_info profile.product_info
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
profile.this_attribute_does_not_exist profile.this_attribute_does_not_exist

View File

@ -1732,8 +1732,3 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
draw.rectangle(xy) draw.rectangle(xy)
with pytest.raises(ValueError): with pytest.raises(ValueError):
draw.rounded_rectangle(xy) draw.rounded_rectangle(xy)
def test_getdraw() -> None:
with pytest.warns(DeprecationWarning):
ImageDraw.getdraw(None, [])

View File

@ -151,11 +151,6 @@ class TestImageFile:
# Despite multiple tiles, assert only one tile caused a read of maxblock size # Despite multiple tiles, assert only one tile caused a read of maxblock size
assert reads.count(im.decodermaxblock) == 1 assert reads.count(im.decodermaxblock) == 1
def test_raise_oserror(self) -> None:
with pytest.warns(DeprecationWarning):
with pytest.raises(OSError):
ImageFile.raise_oserror(1)
def test_raise_typeerror(self) -> None: def test_raise_typeerror(self) -> None:
with pytest.raises(TypeError): with pytest.raises(TypeError):
parser = ImageFile.Parser() parser = ImageFile.Parser()

View File

@ -11,7 +11,6 @@ from pathlib import Path
from typing import Any, BinaryIO from typing import Any, BinaryIO
import pytest import pytest
from packaging.version import parse as parse_version
from PIL import Image, ImageDraw, ImageFont, features from PIL import Image, ImageDraw, ImageFont, features
from PIL._typing import StrOrBytesPath from PIL._typing import StrOrBytesPath
@ -267,6 +266,23 @@ def test_render_multiline_text_align(
assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01)
def test_render_multiline_text_justify_anchor(
font: ImageFont.FreeTypeFont,
) -> None:
im = Image.new("RGB", (280, 240))
draw = ImageDraw.Draw(im)
for xy, anchor in (((0, 0), "la"), ((140, 80), "ma"), ((280, 160), "ra")):
draw.multiline_text(
xy,
"hey you you are awesome\nthis looks awkward\nthis\nlooks awkward",
font=font,
anchor=anchor,
align="justify",
)
assert_image_equal_tofile(im, "Tests/images/multiline_text_justify_anchor.png")
def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: def test_unknown_align(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100)) im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -674,16 +690,6 @@ def test_complex_font_settings() -> None:
def test_variation_get(font: ImageFont.FreeTypeFont) -> 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): with pytest.raises(OSError):
font.get_variation_names() font.get_variation_names()
with pytest.raises(OSError): with pytest.raises(OSError):
@ -746,14 +752,6 @@ def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None
def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> 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): with pytest.raises(OSError):
font.set_variation_by_name("Bold") font.set_variation_by_name("Bold")
@ -773,14 +771,6 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
def test_variation_set_by_axes(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): with pytest.raises(OSError):
font.set_variation_by_axes([500, 50]) font.set_variation_by_axes([500, 50])
@ -1175,15 +1165,15 @@ def test_oom(test_file: str) -> None:
def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
with pytest.warns(UserWarning) as record: with pytest.warns(
UserWarning,
match="Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout.",
):
font = ImageFont.truetype( font = ImageFont.truetype(
FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM
) )
assert font.layout_engine == ImageFont.Layout.BASIC assert font.layout_engine == ImageFont.Layout.BASIC
assert str(record[-1].message) == (
"Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout."
)
@pytest.mark.parametrize("size", [-1, 0]) @pytest.mark.parametrize("size", [-1, 0])
@ -1192,15 +1182,3 @@ def test_invalid_truetype_sizes_raise_valueerror(
) -> None: ) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) 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):
ImageFont.truetype(FONT_PATH, FONT_SIZE)

View File

@ -4,7 +4,11 @@ import pytest
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from .helper import assert_image_similar_tofile, skip_unless_feature from .helper import (
assert_image_equal_tofile,
assert_image_similar_tofile,
skip_unless_feature,
)
FONT_SIZE = 20 FONT_SIZE = 20
FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
@ -354,11 +358,27 @@ def test_combine_multiline(anchor: str, align: str) -> None:
d.line(((200, 0), (200, 400)), "gray") d.line(((200, 0), (200, 400)), "gray")
bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align) bbox = d.multiline_textbbox((200, 200), text, anchor=anchor, font=f, align=align)
d.rectangle(bbox, outline="red") d.rectangle(bbox, outline="red")
d.multiline_text((200, 200), text, fill="black", anchor=anchor, font=f, align=align) d.multiline_text((200, 200), text, "black", anchor=anchor, font=f, align=align)
assert_image_similar_tofile(im, path, 0.015) assert_image_similar_tofile(im, path, 0.015)
def test_combine_multiline_ttb() -> None:
path = "Tests/images/test_combine_multiline_ttb.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
text = "te\nxt"
im = Image.new("RGB", (400, 400), "white")
d = ImageDraw.Draw(im)
d.line(((0, 200), (400, 200)), "gray")
d.line(((200, 0), (200, 400)), "gray")
bbox = d.multiline_textbbox((200, 200), text, f, direction="ttb")
d.rectangle(bbox, outline="red")
d.multiline_text((200, 200), text, "black", f, direction="ttb")
assert_image_equal_tofile(im, path)
def test_anchor_invalid_ttb() -> None: def test_anchor_invalid_ttb() -> None:
font = ImageFont.truetype(FONT_PATH, FONT_SIZE) font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new("RGB", (100, 100), "white") im = Image.new("RGB", (100, 100), "white")
@ -378,8 +398,3 @@ def test_anchor_invalid_ttb() -> None:
d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
with pytest.raises(ValueError): with pytest.raises(ValueError):
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
# ttb multiline text does not support anchors at all
with pytest.raises(ValueError):
d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb")
with pytest.raises(ValueError):
d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb")

View File

@ -2,8 +2,6 @@ from __future__ import annotations
from typing import Any from typing import Any
import pytest
from PIL import Image, ImageMath from PIL import Image, ImageMath
@ -55,11 +53,6 @@ def test_sanity() -> None:
) )
def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.lambda_eval(lambda args: 1, images) == 1
def test_ops() -> None: def test_ops() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1" assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1"

View File

@ -35,16 +35,6 @@ def test_sanity() -> None:
assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3" assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3"
def test_eval_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.eval("1") == 1
def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.unsafe_eval("1", images) == 1
def test_ops() -> None: def test_ops() -> None:
assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1" assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2" assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2"

View File

@ -361,16 +361,6 @@ class TestLibUnpack:
"RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233)
) )
def test_BGR(self) -> None:
with pytest.warns(DeprecationWarning):
self.assert_unpack(
"BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)
)
self.assert_unpack(
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
)
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
def test_RGBA(self) -> None: 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("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
self.assert_unpack( self.assert_unpack(

View File

@ -7,6 +7,7 @@ import sys
import pytest import pytest
@pytest.mark.skipif(sys.platform == "ios", reason="Processes not supported on iOS")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"args, report", "args, report",
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)), ((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),

View File

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from typing import TYPE_CHECKING
import pytest import pytest
@ -9,6 +8,7 @@ from PIL import Image, _typing
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
import numpy import numpy
import numpy.typing as npt import numpy.typing as npt

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from importlib.metadata import metadata
import pytest import pytest
from PIL import __version__ from PIL import __version__
@ -9,7 +11,7 @@ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
def test_pyroma() -> None: def test_pyroma() -> None:
# Arrange # Arrange
data = pyroma.projectdata.get_data(".") data = pyroma.projectdata.map_metadata_keys(metadata("Pillow"))
# Act # Act
rating = pyroma.ratings.rate(data) rating = pyroma.ratings.rate(data)
@ -23,11 +25,5 @@ def test_pyroma() -> None:
) )
else: else:
# Should have a perfect score, but pyroma does not support PEP 639 yet. # Should have a perfect score
assert rating == ( assert rating == (10, [])
9,
[
"Your package does neither have a license field "
"nor any license classifiers."
],
)

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Union from typing import Union
import pytest import pytest
@ -9,6 +9,7 @@ from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper from .helper import assert_image_equal_tofile, assert_image_similar, hopper
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
import PyQt6 import PyQt6
import PySide6 import PySide6

View File

@ -13,7 +13,7 @@ iterations = 5000
When run on a system without the jpeg leak fixes, When run on a system without the jpeg leak fixes,
the valgrind runs look like this. 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
""" """

View File

@ -4,12 +4,11 @@ import platform
import sys import sys
from PIL import features from PIL import features
from Tests.helper import is_pypy
from .helper import is_pypy
def test_wheel_modules() -> None: def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"}
if sys.platform == "win32": if sys.platform == "win32":
# tkinter is not available in cibuildwheel installed CPython on Windows # tkinter is not available in cibuildwheel installed CPython on Windows
@ -20,6 +19,15 @@ def test_wheel_modules() -> None:
except ImportError: except ImportError:
expected_modules.remove("tkinter") expected_modules.remove("tkinter")
# libavif is not available on Windows for ARM64 architectures
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 assert set(features.get_supported_modules()) == expected_modules
@ -31,9 +39,6 @@ def test_wheel_codecs() -> None:
def test_wheel_features() -> None: def test_wheel_features() -> None:
expected_features = { expected_features = {
"webp_anim",
"webp_mux",
"transp_webp",
"raqm", "raqm",
"fribidi", "fribidi",
"harfbuzz", "harfbuzz",
@ -46,5 +51,9 @@ def test_wheel_features() -> None:
expected_features.remove("xcb") expected_features.remove("xcb")
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm": elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
expected_features.remove("zlib_ng") 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 assert set(features.get_supported_features()) == expected_features

View File

@ -16,6 +16,5 @@ coverage:
# Matches 'omit:' in .coveragerc # Matches 'omit:' in .coveragerc
ignore: ignore:
- "Tests/32bit_segfault_check.py" - "checks/*.py"
- "Tests/check_*.py"
- "Tests/createfontdatachunk.py" - "Tests/createfontdatachunk.py"

View File

@ -8,8 +8,8 @@ from __future__ import annotations
import re import re
import subprocess import subprocess
from typing import TYPE_CHECKING
TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from sphinx.application import Sphinx from sphinx.application import Sphinx

View File

@ -12,35 +12,103 @@ Deprecated features
Below are features which are considered deprecated. Where appropriate, Below are features which are considered deprecated. Where appropriate,
a :py:exc:`DeprecationWarning` is issued. a :py:exc:`DeprecationWarning` is issued.
ImageDraw.getdraw hints parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
ExifTags.IFD.Makernote
^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.1.0
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
Image.Image.get_child_images()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.2.1
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.
Image.fromarray mode parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.3.0
The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The
mode can be automatically determined from the object's shape and type instead.
Saving I mode images as PNG
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.3.0
In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
changing the data, this is now deprecated. Instead, the image can be converted to
another mode before saving::
from PIL import Image
im = Image.new("I", (1, 1))
im.convert("I;16").save("out.png")
ImageCms.ImageCmsProfile.product_name and .product_info
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 12.0.0
``ImageCms.ImageCmsProfile.product_name`` and the corresponding
``.product_info`` attributes have been deprecated, and will be removed in
Pillow 13 (2026-10-15). These attributes can be accessed on the ``.profile``
attribute of ``ImageCmsProfile`` instead.
Note that ``.product_name`` and ``.product_info`` have been set to ``None`` on
``ImageCmsProfile`` since Pillow 2.3.0 (2014-01-01), so any working code that
makes use of this data will already access it on ``.profile``.
Removed features
----------------
Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed.
ImageFile.raise_oserror ImageFile.raise_oserror
~~~~~~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.2.0 .. deprecated:: 10.2.0
.. versionremoved:: 12.0.0
``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow ``ImageFile.raise_oserror()`` has been removed. The function was undocumented and was
12.0.0 (2025-10-15). The function is undocumented and is only useful for translating only useful for translating error codes returned by a codec's ``decode()`` method,
error codes returned by a codec's ``decode()`` method, which ImageFile already does which ImageFile already did automatically.
automatically.
IptcImageFile helper functions IptcImageFile helper functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.2.0 .. deprecated:: 10.2.0
.. versionremoved:: 12.0.0
The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow ``IptcImageFile.PAD`` have been removed. These were undocumented helper functions
12.0.0 (2025-10-15). These are undocumented helper functions intended intended for internal use, so there is no replacement. They can each be replaced by a
for internal use, so there is no replacement. They can each be replaced single line of code using builtin functions in Python.
by a single line of code using builtin functions in Python.
ImageCms constants and versions() function ImageCms constants and versions() function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.3.0 .. deprecated:: 10.3.0
.. versionremoved:: 12.0.0
A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. A number of constants and a function in :py:mod:`.ImageCms` have been removed. This
This includes a table of flags based on LittleCMS version 1 which has been includes a table of flags based on LittleCMS version 1 which has been replaced with a
replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags.
============================================ ==================================================== ============================================ ====================================================
Deprecated Use instead Deprecated Use instead
@ -74,48 +142,54 @@ ImageMath eval()
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
.. deprecated:: 10.3.0 .. deprecated:: 10.3.0
.. versionremoved:: 12.0.0
``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or ``ImageMath.eval()`` has been removed. Use :py:meth:`~PIL.ImageMath.lambda_eval` or
:py:meth:`~PIL.ImageMath.unsafe_eval` instead. :py:meth:`~PIL.ImageMath.unsafe_eval` instead.
BGR;15, BGR 16 and BGR;24 BGR;15, BGR 16 and BGR;24
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0 .. deprecated:: 10.4.0
.. versionremoved:: 12.0.0
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated. The experimental BGR;15, BGR;16 and BGR;24 modes have been removed.
Non-image modes in ImageCms Non-image modes in ImageCms
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0 .. deprecated:: 10.4.0
.. versionremoved:: 12.0.0
The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow 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 image modes has been removed. Defaulting to "L" or "1" if the mode cannot be mapped has
is also deprecated. also been removed.
Support for LibTIFF earlier than 4 Support for LibTIFF earlier than 4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0 .. deprecated:: 10.4.0
.. versionremoved:: 12.0.0
Support for LibTIFF earlier than version 4 has been deprecated. Support for LibTIFF earlier than version 4 has been removed.
Upgrade to a newer version of LibTIFF instead. Upgrade to a newer version of LibTIFF instead.
ImageDraw.getdraw hints parameter ImageDraw.getdraw hints parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0 .. deprecated:: 10.4.0
.. versionremoved:: 12.0.0
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been removed.
FreeType 2.9.0 FreeType 2.9.0
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
.. deprecated:: 11.0.0 .. deprecated:: 11.0.0
.. versionremoved:: 12.0.0
Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0 Support for FreeType 2.9.0 has been removed. FreeType 2.9.1 is the minimum version
(2025-10-15), when FreeType 2.9.1 will be the minimum supported. supported.
We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe
vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
@ -128,111 +202,62 @@ ICNS (width, height, scale) sizes
.. deprecated:: 11.0.0 .. deprecated:: 11.0.0
Setting an ICNS image size to ``(width, height, scale)`` before loading has been Setting an ICNS image size to ``(width, height, scale)`` before loading has been
deprecated. Instead, ``load(scale)`` can be used. removed. Instead, ``load(scale)`` can be used.
Image isImageType() Image isImageType()
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0 .. deprecated:: 11.0.0
.. versionremoved:: 12.0.0
``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)`` ``Image.isImageType(im)`` has been removed. Use ``isinstance(im, Image.Image)``
instead. instead.
ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0 .. deprecated:: 11.0.0
.. versionremoved:: 12.0.0
The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword :py:meth:`~PIL.ImageMath.unsafe_eval()` has been removed. One or more keyword
arguments can be used instead. arguments can be used instead.
JpegImageFile.huffman_ac and JpegImageFile.huffman_dc JpegImageFile.huffman_ac and JpegImageFile.huffman_dc
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0 .. deprecated:: 11.0.0
.. versionremoved:: 12.0.0
The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They 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). have been removed.
Specific WebP feature checks Specific WebP feature checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0 .. deprecated:: 11.0.0
.. versionremoved:: 12.0.0
``features.check("transp_webp")``, ``features.check("webp_mux")`` and ``features.check("transp_webp")``, ``features.check("webp_mux")`` and
``features.check("webp_anim")`` are now deprecated. They will always return ``features.check("webp_anim")`` have been removed.
``True`` if the WebP module is installed, until they are removed in Pillow
12.0.0 (2025-10-15).
Get internal pointers to objects Get internal pointers to objects
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0 .. deprecated:: 11.0.0
.. versionremoved:: 12.0.0
``Image.core.ImagingCore.id`` and ``Image.core.ImagingCore.unsafe_ptrs`` have been ``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 removed. They were used for obtaining raw pointers to ``ImagingCore`` internals. To
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use interact with C code, you can use ``Image.Image.getim()``, which returns a ``Capsule``
``Image.Image.getim()``, which returns a ``Capsule`` object. object.
ExifTags.IFD.Makernote
^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.1.0
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
Image.Image.get_child_images()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.2.1
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.
Saving I mode images as PNG
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.3.0
In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
changing the data, this is now deprecated. Instead, the image can be converted to
another mode before saving::
from PIL import Image
im = Image.new("I", (1, 1))
im.convert("I;16").save("out.png")
ImageCms.ImageCmsProfile.product_name and .product_info
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.3.0
``ImageCms.ImageCmsProfile.product_name`` and the corresponding
``.product_info`` attributes have been deprecated, and will be removed in
Pillow 13 (2026-10-15). These attributes can be accessed on the ``.profile``
attribute of ``ImageCmsProfile`` instead.
Note that ``.product_name`` and ``.product_info`` have been set to ``None`` on
``ImageCmsProfile`` since Pillow 2.3.0 (2014-01-01), so any working code that
makes use of this data will already access it on ``.profile``.
Removed features
----------------
Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed.
TiffImagePlugin IFD_LEGACY_API TiffImagePlugin IFD_LEGACY_API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 11.0.0 .. 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 PSFile
~~~~~~ ~~~~~~

View File

@ -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 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 files and will retain the original image quality level, subsampling, and
qtables. qtables.
For more information on how qtables are modified based on the quality parameter,
see the qtables section.
**optimize** **optimize**
If present and true, indicates that the encoder should make an extra pass 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 range(len(keys))) of lists of 64 integers. There must be
between 2 and 4 tables. 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 .. versionadded:: 2.5.0
**streamtype** **streamtype**
@ -1661,7 +1668,8 @@ handler. ::
XPM XPM
^^^ ^^^
Pillow reads X pixmap files (mode ``P``) with 256 colors or less. Pillow reads X pixmap files as P mode images if there are 256 colors or less, and as
RGB images otherwise.
.. _xpm-opening: .. _xpm-opening:

View File

@ -75,7 +75,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | 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 | | | | 3.8 | 10.4.0 | |
+----------------------------------+----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+

View File

@ -56,7 +56,6 @@ Functions
.. autofunction:: get_display_profile .. autofunction:: get_display_profile
.. autofunction:: isIntentSupported .. autofunction:: isIntentSupported
.. autofunction:: profileToProfile .. autofunction:: profileToProfile
.. autofunction:: versions
CmsProfile CmsProfile
---------- ----------

View File

@ -21,9 +21,7 @@ with any Arrow provider or consumer in the Python ecosystem.
Data formats Data formats
============ ============
Pillow currently supports exporting Arrow images in all modes 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.
For single-band images, the exported array is width*height elements, For single-band images, the exported array is width*height elements,
with each pixel corresponding to the appropriate Arrow type. with each pixel corresponding to the appropriate Arrow type.

View File

@ -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. * ``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. * ``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. * ``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.check_feature
.. autofunction:: PIL.features.version_feature .. autofunction:: PIL.features.version_feature

View File

@ -4,25 +4,31 @@
Security Security
======== ========
TODO :cve:`2025-48379`: Write buffer overflow on BCn encoding
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO There is a heap buffer overflow when writing a sufficiently large (>64k encoded with
default settings) image in the DDS format due to writing into a buffer without checking
for available space.
:cve:`YYYY-XXXXX`: TODO This only affects users who save untrusted data as a compressed DDS image.
^^^^^^^^^^^^^^^^^^^^^^^
TODO * Unclear how large the potential write could be. It is likely limited by process
segfault, so it's not necessarily deterministic. It may be practically unbounded.
* Unclear if there's a restriction on the bytes that could be emitted. It's likely that
the only restriction is that the bytes would be emitted in chunks of 8 or 16.
Backwards incompatible changes This was introduced in Pillow 11.2.0 when the feature was added.
==============================
TODO
^^^^
Deprecations Deprecations
============ ============
Image.fromarray mode parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The
mode can be automatically determined from the object's shape and type instead.
Saving I mode images as PNG Saving I mode images as PNG
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -35,22 +41,6 @@ another mode before saving::
im = Image.new("I", (1, 1)) im = Image.new("I", (1, 1))
im.convert("I;16").save("out.png") im.convert("I;16").save("out.png")
API changes
===========
TODO
^^^^
TODO
API additions
=============
TODO
^^^^
TODO
Other changes Other changes
============= =============
@ -74,6 +64,20 @@ Pillow only supports libavif 1.0.0 or later. In order to prevent errors when bui
from source, if a user happens to have an earlier libavif on their system, Pillow will from source, if a user happens to have an earlier libavif on their system, Pillow will
now ignore it. now ignore it.
AVIF support in wheels
^^^^^^^^^^^^^^^^^^^^^^
Support for reading and writing AVIF images is now included in Pillow's wheels, except
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 Python 3.14 beta
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^

View File

@ -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

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
12.0.0
11.3.0 11.3.0
11.2.1 11.2.1
11.1.0 11.1.0

View File

@ -20,6 +20,8 @@ Backwards incompatible changes
TODO TODO
^^^^ ^^^^
TODO
Deprecations Deprecations
============ ============

14
patches/README.md Normal file
View File

@ -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.

View File

@ -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 <endian.h>
#elif defined(OS_FREEBSD)
#include <machine/endian.h>
-#elif defined(OS_MACOSX)
+#elif defined(OS_MACOSX) || defined(OS_IOS)
#include <machine/endian.h>
/* Let's try and follow the Linux convention */
#define BROTLI_X_BYTE_ORDER BYTE_ORDER

View File

@ -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

View File

@ -9,7 +9,7 @@ backend-path = [
[project] [project]
name = "pillow" name = "pillow"
description = "Python Imaging Library (Fork)" description = "Python Imaging Library (fork)"
readme = "README.md" readme = "README.md"
keywords = [ keywords = [
"Imaging", "Imaging",
@ -103,15 +103,55 @@ before-all = ".github/workflows/wheels-dependencies.sh"
build-verbosity = 1 build-verbosity = 1
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" 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-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests" 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] [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" 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] [tool.black]
exclude = "wheels/multibuild" exclude = "wheels/multibuild"
@ -168,7 +208,7 @@ lint.isort.required-imports = [
max_supported_python = "3.13" max_supported_python = "3.13"
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "-ra --color=yes" addopts = "-ra --color=auto"
testpaths = [ testpaths = [
"Tests", "Tests",
] ]

View File

@ -16,7 +16,6 @@ import subprocess
import sys import sys
import warnings import warnings
from collections.abc import Iterator from collections.abc import Iterator
from typing import Any
from setuptools import Extension, setup from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext from setuptools.command.build_ext import build_ext
@ -148,7 +147,7 @@ class RequiredDependencyException(Exception):
PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version
def _dbg(s: str, tp: Any = None) -> None: def _dbg(s: str, tp: str | tuple[str, ...] | None = None) -> None:
if DEBUG: if DEBUG:
if tp: if tp:
print(s % tp) print(s % tp)
@ -474,6 +473,19 @@ class pil_build_ext(build_ext):
sdk_path = commandlinetools_sdk_path sdk_path = commandlinetools_sdk_path
return 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: def build_extensions(self) -> None:
library_dirs: list[str] = [] library_dirs: list[str] = []
include_dirs: list[str] = [] include_dirs: list[str] = []
@ -509,11 +521,11 @@ class pil_build_ext(build_ext):
if root is None and pkg_config: if root is None and pkg_config:
if isinstance(lib_name, str): if isinstance(lib_name, str):
_dbg(f"Looking for `{lib_name}` using pkg-config.") _dbg("Looking for `%s` using pkg-config.", lib_name)
root = pkg_config(lib_name) root = pkg_config(lib_name)
else: else:
for lib_name2 in lib_name: for lib_name2 in lib_name:
_dbg(f"Looking for `{lib_name2}` using pkg-config.") _dbg("Looking for `%s` using pkg-config.", lib_name2)
root = pkg_config(lib_name2) root = pkg_config(lib_name2)
if root: if root:
break break
@ -623,6 +635,18 @@ class pil_build_ext(build_ext):
for extension in self.extensions: for extension in self.extensions:
extension.extra_compile_args = ["-Wno-nullability-completeness"] 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")): elif sys.platform.startswith(("linux", "gnu", "freebsd")):
for dirname in _find_library_dirs_ldconfig(): for dirname in _find_library_dirs_ldconfig():
_add_directory(library_dirs, dirname) _add_directory(library_dirs, dirname)
@ -732,7 +756,7 @@ class pil_build_ext(build_ext):
best_path = os.path.join(directory, name) best_path = os.path.join(directory, name)
_dbg( _dbg(
"Best openjpeg version %s so far in %s", "Best openjpeg version %s so far in %s",
(best_version, best_path), (str(best_version), best_path),
) )
if best_version and _find_library_file(self, "openjp2"): if best_version and _find_library_file(self, "openjp2"):
@ -754,12 +778,12 @@ class pil_build_ext(build_ext):
if feature.want("tiff"): if feature.want("tiff"):
_dbg("Looking for tiff") _dbg("Looking for tiff")
if _find_include_file(self, "tiff.h"): if _find_include_file(self, "tiff.h"):
if _find_library_file(self, "tiff"):
feature.set("tiff", "tiff")
if sys.platform in ["win32", "darwin"] and _find_library_file( if sys.platform in ["win32", "darwin"] and _find_library_file(
self, "libtiff" self, "libtiff"
): ):
feature.set("tiff", "libtiff") feature.set("tiff", "libtiff")
elif _find_library_file(self, "tiff"):
feature.set("tiff", "tiff")
if feature.want("freetype"): if feature.want("freetype"):
_dbg("Looking for freetype") _dbg("Looking for freetype")
@ -878,6 +902,9 @@ class pil_build_ext(build_ext):
# so we have to guess; by default it is defined in all Windows builds. # so we have to guess; by default it is defined in all Windows builds.
# See #4237, #5243, #5359 for more information. # See #4237, #5243, #5359 for more information.
defs.append(("USE_WIN32_FILEIO", None)) defs.append(("USE_WIN32_FILEIO", None))
elif sys.platform == "ios":
# Ensure transitive dependencies are linked.
libs.append("lzma")
if feature.get("jpeg"): if feature.get("jpeg"):
libs.append(feature.get("jpeg")) libs.append(feature.get("jpeg"))
defs.append(("HAVE_LIBJPEG", None)) defs.append(("HAVE_LIBJPEG", None))
@ -894,6 +921,9 @@ class pil_build_ext(build_ext):
defs.append(("HAVE_LIBIMAGEQUANT", None)) defs.append(("HAVE_LIBIMAGEQUANT", None))
if feature.get("xcb"): if feature.get("xcb"):
libs.append(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)) defs.append(("HAVE_XCB", None))
if sys.platform == "win32": if sys.platform == "win32":
libs.extend(["kernel32", "user32", "gdi32"]) libs.extend(["kernel32", "user32", "gdi32"])
@ -925,6 +955,11 @@ class pil_build_ext(build_ext):
libs.append(feature.get("fribidi")) libs.append(feature.get("fribidi"))
else: # building FriBiDi shim from src/thirdparty else: # building FriBiDi shim from src/thirdparty
srcs.append("src/thirdparty/fribidi-shim/fribidi.c") 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) self._update_extension("PIL._imagingft", libs, defs, srcs)
else: else:
@ -941,6 +976,9 @@ class pil_build_ext(build_ext):
webp = feature.get("webp") webp = feature.get("webp")
if isinstance(webp, str): if isinstance(webp, str):
libs = [webp, webp + "mux", webp + "demux"] libs = [webp, webp + "mux", webp + "demux"]
if sys.platform == "ios":
# Ensure transitive dependencies are linked.
libs.append("sharpyuv")
self._update_extension("PIL._webp", libs) self._update_extension("PIL._webp", libs)
else: else:
self._remove_extension("PIL._webp") self._remove_extension("PIL._webp")

View File

@ -445,9 +445,9 @@ def _save(
image = stride * im.size[1] image = stride * im.size[1]
if im.mode == "1": if im.mode == "1":
palette = b"".join(o8(i) * 4 for i in (0, 255)) palette = b"".join(o8(i) * 3 + b"\x00" for i in (0, 255))
elif im.mode == "L": elif im.mode == "L":
palette = b"".join(o8(i) * 4 for i in range(256)) palette = b"".join(o8(i) * 3 + b"\x00" for i in range(256))
elif im.mode == "P": elif im.mode == "P":
palette = im.im.getpalette("RGB", "BGRX") palette = im.im.getpalette("RGB", "BGRX")
colors = len(palette) // 4 colors = len(palette) // 4

View File

@ -480,8 +480,11 @@ class GifImageFile(ImageFile.ImageFile):
self._prev_im = expanded_im self._prev_im = expanded_im
assert self._prev_im is not None assert self._prev_im is not None
if self._frame_transparency is not None: if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0) if self.mode == "L":
frame_im = self.im.convert("RGBA") 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: else:
frame_im = self.im.convert("RGB") frame_im = self.im.convert("RGB")
@ -490,7 +493,7 @@ class GifImageFile(ImageFile.ImageFile):
self.im = self._prev_im self.im = self._prev_im
self._mode = self.im.mode 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) self.im.paste(frame_im, self.dispose_extent, frame_im)
else: else:
self.im.paste(frame_im, self.dispose_extent) self.im.paste(frame_im, self.dispose_extent)

View File

@ -25,7 +25,6 @@ import sys
from typing import IO from typing import IO
from . import Image, ImageFile, PngImagePlugin, features from . import Image, ImageFile, PngImagePlugin, features
from ._deprecate import deprecate
enable_jpeg2k = features.check_codec("jpg_2000") enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k: if enable_jpeg2k:
@ -275,34 +274,25 @@ class IcnsImageFile(ImageFile.ImageFile):
self.best_size[1] * self.best_size[2], self.best_size[1] * self.best_size[2],
) )
@property # type: ignore[override] @property
def size(self) -> tuple[int, int] | tuple[int, int, int]: def size(self) -> tuple[int, int]:
return self._size return self._size
@size.setter @size.setter
def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None: def size(self, value: tuple[int, int]) -> None:
if len(value) == 3: # Check that a matching size exists,
deprecate("Setting size to (width, height, scale)", 12, "load(scale)") # or that there is a scale that would create a size that matches
if value in self.info["sizes"]: for size in self.info["sizes"]:
self._size = value # type: ignore[assignment] 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 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" msg = "This is not one of the allowed sizes of this image"
raise ValueError(msg) raise ValueError(msg)
def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: def load(self, scale: int | None = None) -> Image.core.PixelAccess | None:
if scale is not None or len(self.size) == 3: if scale is not None:
if scale is None and len(self.size) == 3:
scale = self.size[2]
assert scale is not None
width, height = self.size[:2] width, height = self.size[:2]
self.size = width * scale, height * scale self.size = width * scale, height * scale
self.best_size = width, height, scale self.best_size = width, height, scale

View File

@ -362,7 +362,7 @@ class IcoImageFile(ImageFile.ImageFile):
self.info["sizes"] = set(sizes) self.info["sizes"] = set(sizes)
self.size = im.size self.size = im.size
return None return Image.Image.load(self)
def load_seek(self, pos: int) -> None: def load_seek(self, pos: int) -> None:
# Flag the ImageFile.Parser so that it # Flag the ImageFile.Parser so that it

View File

@ -115,21 +115,6 @@ except ImportError as v:
raise 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 # Constants
@ -219,7 +204,7 @@ if TYPE_CHECKING:
from IPython.lib.pretty import PrettyPrinter from IPython.lib.pretty import PrettyPrinter
from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
from ._typing import CapsuleType, NumpyArray, StrOrBytesPath, TypeGuard from ._typing import CapsuleType, NumpyArray, StrOrBytesPath
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
str, str,
@ -980,9 +965,6 @@ class Image:
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
""" """
if mode in ("BGR;15", "BGR;16", "BGR;24"):
deprecate(mode, 12)
self.load() self.load()
has_transparency = "transparency" in self.info has_transparency = "transparency" in self.info
@ -2229,8 +2211,6 @@ class Image:
:py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
:py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`.
If the image has mode "1" or "P", it is always set to 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.NEAREST`. Otherwise, the default filter is
:py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`.
:param box: An optional 4-tuple of floats providing :param box: An optional 4-tuple of floats providing
@ -2253,8 +2233,7 @@ class Image:
""" """
if resample is None: if resample is None:
bgr = self.mode.startswith("BGR;") resample = Resampling.BICUBIC
resample = Resampling.NEAREST if bgr else Resampling.BICUBIC
elif resample not in ( elif resample not in (
Resampling.NEAREST, Resampling.NEAREST,
Resampling.BILINEAR, Resampling.BILINEAR,
@ -2556,7 +2535,9 @@ class Image:
self.load() self.load()
save_all = params.pop("save_all", None) save_all = params.pop("save_all", None)
self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} self._default_encoderinfo = params
encoderinfo = getattr(self, "encoderinfo", {})
self._attach_default_encoderinfo(self)
self.encoderconfig: tuple[Any, ...] = () self.encoderconfig: tuple[Any, ...] = ()
if format.upper() not in SAVE: if format.upper() not in SAVE:
@ -2594,13 +2575,15 @@ class Image:
pass pass
raise raise
finally: finally:
try: self.encoderinfo = encoderinfo
del self.encoderinfo
except AttributeError:
pass
if open_fp: if open_fp:
fp.close() 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 seek(self, frame: int) -> None: def seek(self, frame: int) -> None:
""" """
Seeks to the given frame in this sequence file. If you seek Seeks to the given frame in this sequence file. If you seek
@ -3081,9 +3064,6 @@ def new(
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
""" """
if mode in ("BGR;15", "BGR;16", "BGR;24"):
deprecate(mode, 12)
_check_size(size) _check_size(size)
if color is None: if color is None:
@ -3272,7 +3252,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
:param obj: Object with array interface :param obj: Object with array interface
:param mode: Optional mode to use when reading ``obj``. Will be determined from :param mode: Optional mode to use when reading ``obj``. Will be determined from
type if ``None``. type if ``None``. Deprecated.
This will not be used to convert the data after reading, but will be used to This will not be used to convert the data after reading, but will be used to
change how the data is read:: change how the data is read::
@ -3307,6 +3287,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" msg = f"Cannot handle this data type: {typekey_shape}, {typestr}"
raise TypeError(msg) from e raise TypeError(msg) from e
else: else:
deprecate("'mode' parameter", 13)
rawmode = mode rawmode = mode
if mode in ["1", "L", "I", "P", "F"]: if mode in ["1", "L", "I", "P", "F"]:
ndmax = 2 ndmax = 2
@ -3505,8 +3486,6 @@ def open(
filename: str | bytes = "" filename: str | bytes = ""
if is_path(fp): if is_path(fp):
filename = os.fspath(fp) filename = os.fspath(fp)
if filename:
fp = builtins.open(filename, "rb") fp = builtins.open(filename, "rb")
exclusive_fp = True exclusive_fp = True
else: else:

View File

@ -25,7 +25,7 @@ from enum import IntEnum, IntFlag
from functools import reduce from functools import reduce
from typing import Any, Literal, SupportsFloat, SupportsInt, Union from typing import Any, Literal, SupportsFloat, SupportsInt, Union
from . import Image, __version__ from . import Image
from ._deprecate import deprecate from ._deprecate import deprecate
from ._typing import SupportsRead from ._typing import SupportsRead
@ -108,20 +108,6 @@ pyCMS
_VERSION = "1.0.0 pil" _VERSION = "1.0.0 pil"
def __getattr__(name: str) -> Any:
if name == "DESCRIPTION":
deprecate("PIL.ImageCms.DESCRIPTION", 12)
return _DESCRIPTION
elif name == "VERSION":
deprecate("PIL.ImageCms.VERSION", 12)
return _VERSION
elif name == "FLAGS":
deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags")
return _FLAGS
msg = f"module '{__name__}' has no attribute '{name}'"
raise AttributeError(msg)
# --------------------------------------------------------------------. # --------------------------------------------------------------------.
@ -316,31 +302,6 @@ class ImageCmsTransform(Image.ImagePointHandler):
proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC, proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
flags: Flags = Flags.NONE, 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: if proof is None:
self.transform = core.buildTransform( self.transform = core.buildTransform(
input.profile, output.profile, input_mode, output_mode, intent, flags input.profile, output.profile, input_mode, output_mode, intent, flags
@ -1123,16 +1084,3 @@ def isIntentSupported(
return -1 return -1
except (AttributeError, OSError, TypeError, ValueError) as v: except (AttributeError, OSError, TypeError, ValueError) as v:
raise PyCMSError(v) from 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__

Some files were not shown because too many files have changed in this diff Show More