Merge branch 'main' into main

This commit is contained in:
Andrew Murray 2025-10-21 08:20:30 +11:00 committed by GitHub
commit 7a161f65a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
302 changed files with 18971 additions and 4034 deletions

View File

@ -13,24 +13,21 @@ aptget_update()
return 1 return 1
fi fi
} }
if [[ $(uname) != CYGWIN* ]]; then aptget_update || aptget_update retry || aptget_update retry
aptget_update || aptget_update retry || aptget_update retry
fi
set -e set -e
if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ sway wl-clipboard libopenblas-dev nasm
sway wl-clipboard libopenblas-dev nasm
fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install ipython python3 -m pip install ipython
python3 -m pip install numpy
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
@ -40,36 +37,24 @@ python3 -m pip install pyroma
# fails on beta 3.14 and PyPy # fails on beta 3.14 and PyPy
python3 -m pip install --only-binary=:all: pyarrow || true python3 -m pip install --only-binary=:all: pyarrow || true
if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3
python3 -m pip install numpy if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
# PyQt6 doesn't support PyPy3 # TODO Update condition when pyqt6 supports free-threading
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
# TODO Update condition when pyqt6 supports free-threading
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
fi
# Pyroma uses non-isolated build and fails with old setuptools
if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
# To match pyproject.toml
python3 -m pip install "setuptools>=77"
fi
# webp
pushd depends && ./install_webp.sh && popd
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
pushd depends && ./install_raqm.sh && popd
# libavif
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
else
cd depends && ./install_extra_test_images.sh && cd ..
fi fi
# webp
pushd depends && ./install_webp.sh && popd
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
pushd depends && sudo ./install_raqm.sh && popd
# libavif
pushd depends && sudo ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -1 +1 @@
cibuildwheel==2.23.3 cibuildwheel==3.2.1

View File

@ -1,9 +1,13 @@
mypy==1.15.0 mypy==1.18.2
arro3-compute
arro3-core
IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6 IceSpringPySideStubs-PySide6
ipython ipython
numpy numpy
packaging packaging
pyarrow-stubs
pybind11
pytest pytest
sphinx sphinx
types-atheris types-atheris

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

1
.github/mergify.yml vendored
View File

@ -8,7 +8,6 @@ pull_request_rules:
- status-success=Docker Test Successful - status-success=Docker Test Successful
- status-success=Windows Test Successful - status-success=Windows Test Successful
- status-success=MinGW - status-success=MinGW
- status-success=Cygwin Test Successful
actions: actions:
merge: merge:
method: merge method: merge

View File

@ -32,12 +32,12 @@ jobs:
name: Docs name: Docs
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip cache: pip

View File

@ -20,7 +20,7 @@ jobs:
name: Lint name: Lint
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
@ -33,7 +33,7 @@ jobs:
lint-pre-commit- lint-pre-commit-
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip cache: pip

View File

@ -2,9 +2,6 @@
set -e set -e
if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven
fi
brew install \ brew install \
aom \ aom \
dav1d \ dav1d \

View File

@ -22,7 +22,7 @@ jobs:
steps: steps:
- name: "Check issues" - name: "Check issues"
uses: actions/stale@v9 uses: actions/stale@v10
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action" only-labels: "Awaiting OP Action"

View File

@ -1,154 +0,0 @@
name: Test Cygwin
on:
push:
branches:
- "**"
paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- "docs/**"
- "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- "docs/**"
- "wheels/**"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs:
build:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
python-minor-version: [9]
timeout-minutes: 40
name: Python 3.${{ matrix.python-minor-version }}
steps:
- name: Fix line endings
run: |
git config --global core.autocrlf input
- name: Checkout Pillow
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Cygwin
uses: cygwin/cygwin-install-action@v5
with:
packages: >
gcc-g++
ghostscript
git
ImageMagick
jpeg
libfreetype-devel
libimagequant-devel
libjpeg-devel
liblapack-devel
liblcms2-devel
libopenjp2-devel
libraqm-devel
libtiff-devel
libwebp-devel
libxcb-devel
libxcb-xinerama0
make
netpbm
perl
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter
wget
xorg-server-extra
zlib-devel
- name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v4
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
- name: Select Python version
run: |
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
- name: pip cache
uses: actions/cache@v4
with:
path: 'C:\cygwin\home\runneradmin\.cache\pip'
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
restore-keys: |
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
- name: Build system information
run: |
dash.exe -c "python3 .github/workflows/system-info.py"
- name: Install dependencies
run: |
bash.exe .ci/install.sh
- name: Build
shell: bash.exe -eo pipefail -o igncr "{0}"
run: |
.ci/build.sh
- name: Test
run: |
bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
- name: Prepare to upload errors
if: failure()
run: |
dash.exe -c "mkdir -p Tests/errors"
- name: Upload errors
uses: actions/upload-artifact@v4
if: failure()
with:
name: errors
path: Tests/errors
- name: After success
run: |
bash.exe .ci/after_success.sh
rm C:\cygwin\bin\bash.EXE
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
flags: GHA_Cygwin
name: Cygwin Python 3.${{ matrix.python-minor-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:
contents: none
needs: build
runs-on: ubuntu-latest
name: Cygwin Test Successful
steps:
- name: Success
run: echo Cygwin Test Successful

View File

@ -47,7 +47,8 @@ jobs:
centos-stream-10-amd64, centos-stream-10-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-41-amd64, debian-13-trixie-x86,
debian-13-trixie-amd64,
fedora-42-amd64, fedora-42-amd64,
gentoo, gentoo,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,
@ -66,7 +67,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@ -45,7 +45,7 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@ -0,0 +1,60 @@
name: Test Valgrind Memory Leaks
# like the Docker tests, but running valgrind only on *.c/*.h changes.
# this is very expensive. Only run on the pull request.
on:
# push:
# branches:
# - "**"
# paths:
# - ".github/workflows/test-valgrind.yml"
# - "**.c"
# - "**.h"
pull_request:
paths:
- ".github/workflows/test-valgrind.yml"
- "**.c"
- "**.h"
- "depends/docker-test-valgrind-memory.sh"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
docker: [
ubuntu-22.04-jammy-amd64-valgrind,
]
dockerTag: [main]
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Docker pull
run: |
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
- name: Build and Run Valgrind
run: |
# The Pillow user in the docker container is UID 1001
sudo chown -R 1001 $GITHUB_WORKSPACE
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh
sudo chown -R runner $GITHUB_WORKSPACE

View File

@ -39,7 +39,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false

View File

@ -31,16 +31,15 @@ env:
jobs: jobs:
build: build:
runs-on: ${{ matrix.os }} runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"]
architecture: ["x64"] architecture: ["x64"]
os: ["windows-latest"]
include: include:
# Test the oldest Python on 32-bit # Test the oldest Python on 32-bit
- { python-version: "3.9", architecture: "x86", os: "windows-2019" } - { python-version: "3.10", architecture: "x86" }
timeout-minutes: 45 timeout-minutes: 45
@ -48,19 +47,19 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
@ -68,7 +67,7 @@ jobs:
# sets env: pythonLocation # sets env: pythonLocation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
@ -98,8 +97,8 @@ jobs:
choco install nasm --no-progress choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.5.1 --no-progress choco install ghostscript --version=10.6.0 --no-progress
echo "C:\Program Files\gs\gs10.05.1\bin" >> $env:GITHUB_PATH echo "C:\Program Files\gs\gs10.06.0\bin" >> $env:GITHUB_PATH
# Install extra test images # Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images xcopy /S /Y Tests\test-images\* Tests\images

View File

@ -42,35 +42,35 @@ jobs:
] ]
python-version: [ python-version: [
"pypy3.11", "pypy3.11",
"pypy3.10", "3.14t",
"3.14", "3.14",
"3.13t", "3.13t",
"3.13", "3.13",
"3.12", "3.12",
"3.11", "3.11",
"3.10", "3.10",
"3.9",
] ]
include: include:
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 } - { python-version: "3.11", PYTHONOPTIMIZE: 2 }
# Free-threaded # Free-threaded
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true } - { python-version: "3.13t", disable-gil: true }
# M1 only available for 3.10+ # Intel
- { os: "macos-13", python-version: "3.9" } - { os: "macos-15-intel", python-version: "3.10" }
exclude: exclude:
- { os: "macos-latest", python-version: "3.9" } - { os: "macos-latest", python-version: "3.10" }
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
@ -111,7 +111,7 @@ jobs:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher - name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'" if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json" run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build - name: Build

View File

@ -1,77 +1,164 @@
#!/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 -DENABLE_SHARED=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"
FREETYPE_VERSION=2.13.3 # annotations have a source code patch that is required for some platforms. If
HARFBUZZ_VERSION=11.2.1 # you change those versions, ensure the patch is also updated.
LIBPNG_VERSION=1.6.48 if [[ -n "$IOS_SDK" ]]; then
JPEGTURBO_VERSION=3.1.0 FREETYPE_VERSION=2.13.3
OPENJPEG_VERSION=2.5.3 else
FREETYPE_VERSION=2.14.1
fi
HARFBUZZ_VERSION=12.1.0
LIBPNG_VERSION=1.6.50
JPEGTURBO_VERSION=3.1.2
OPENJPEG_VERSION=2.5.4
XZ_VERSION=5.8.1 XZ_VERSION=5.8.1
TIFF_VERSION=4.7.0 ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.1
LCMS2_VERSION=2.17 LCMS2_VERSION=2.17
ZLIB_VERSION=1.3.1 ZLIB_NG_VERSION=2.2.5
ZLIB_NG_VERSION=2.2.4 LIBWEBP_VERSION=1.6.0
LIBWEBP_VERSION=1.5.0
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,8 +168,8 @@ 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 -j4 install)
touch brotli-stamp touch brotli-stamp
} }
@ -92,46 +179,129 @@ 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" ]] && [[ -z "$IOS_SDK" ]]) || [ -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 build_shared=ON
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" \
)
if [[ -n "$IOS_SDK" ]]; then
build_shared=OFF
fi
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
if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then
libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic)
else
libavif_cmake_flags+=(
-DAVIF_CODEC_AOM_DECODE=OFF \
-DAVIF_CODEC_DAV1D=LOCAL
)
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=$build_shared \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_AOM=LOCAL \
-DCONFIG_AV1_HIGHBITDEPTH=0 \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
-DCMAKE_C_VISIBILITY_PRESET=hidden \
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \
-DCMAKE_BUILD_TYPE=$build_type \
"${libavif_cmake_flags[@]}" \
$HOST_CMAKE_FLAGS . )
if [[ -n "$IOS_SDK" ]]; then
# libavif's CMake configuration generates a meson cross file... but it
# doesn't work for iOS cross-compilation. Copy in Pillow-generated
# meson-cross config to replace the cmake-generated version.
cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson
fi
(cd $out_dir && make -j4 install)
touch libavif-stamp
}
function build_zstd {
if [ -e zstd-stamp ]; then return; fi
local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz)
(cd $out_dir \
&& make -j4 install)
touch zstd-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
yum remove -y zlib-devel yum remove -y zlib-devel
fi fi
if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then if [[ -n "$IS_MACOS" ]]; then
build_new_zlib CFLAGS="$CFLAGS -headerpad_max_install_names" build_zlib_ng
else else
build_zlib_ng build_zlib_ng
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 \
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
--disable-webp --disable-libdeflate --disable-zstd --disable-webp --disable-libdeflate --disable-zstd
else else
build_zstd
build_tiff build_tiff
fi fi
build_libavif
build_libpng build_libpng
build_lcms2 build_lcms2
build_openjpeg build_openjpeg
@ -140,20 +310,58 @@ function build {
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names" webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
fi fi
CFLAGS="$CFLAGS $webp_cflags" build_simple libwebp $LIBWEBP_VERSION \ webp_ldflags=""
if [[ -n "$IOS_SDK" ]]; then
webp_ldflags="$webp_ldflags -llzma -lz"
fi
CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \ https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
--enable-libwebpmux --enable-libwebpdemux --enable-libwebpmux --enable-libwebpdemux
build_brotli build_brotli
if [ -n "$IS_MACOS" ]; then if [[ -n "$IS_MACOS" ]]; then
# Custom freetype build # Custom freetype build
if [[ -z "$IOS_SDK" ]]; then
build_simple sed 4.9 https://mirrors.middlendian.com/gnu/sed
fi
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,28 +380,53 @@ 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
# A safety catch for iOS. iOS can't use dynamic libraries, but clang will prefer
# to link dynamic libraries to static libraries. The only way to reliably
# prevent this is to not have dynamic libraries available in the first place.
# The build process *shouldn't* generate any dylibs... but just in case, purge
# any dylibs that *have* been installed into the build prefix directory.
if [[ -n "$IOS_SDK" ]]; then
find "$BUILD_PREFIX" -name "*.dylib" -exec rm -rf {} \;
fi
# Return to the project root to finish the build # Return to the project root to finish the build
popd > /dev/null popd > /dev/null

View File

@ -9,17 +9,18 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
} }
$env:path += ";$pillow\winbuild\build\bin\" $env:path += ";$pillow\winbuild\build\bin\"
& "$venv\Scripts\activate.ps1" if (Test-Path $venv\Scripts\pypy.exe) {
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f $python = "pypy.exe"
if ("$venv" -like "*\cibw-run-*-win_amd64\*") { } else {
& python -m pip install numpy $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
cd $pillow cd $pillow
& python -VV & $venv\Scripts\$python -VV
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& python selftest.py & $venv\Scripts\$python selftest.py
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& 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 }
& 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

@ -39,6 +39,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
EXPECTED_DISTS: 91
FORCE_COLOR: 1 FORCE_COLOR: 1
jobs: jobs:
@ -51,47 +52,67 @@ jobs:
matrix: matrix:
include: include:
- name: "macOS 10.10 x86_64" - name: "macOS 10.10 x86_64"
os: macos-13 platform: macos
os: macos-15-intel
cibw_arch: x86_64 cibw_arch: x86_64
build: "cp3{9,10,11}*" build: "cp3{10,11}*"
macosx_deployment_target: "10.10" macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64" - name: "macOS 10.13 x86_64"
os: macos-13 platform: macos
os: macos-15-intel
cibw_arch: x86_64 cibw_arch: x86_64
build: "cp3{12,13}*" build: "cp3{12,13}*"
macosx_deployment_target: "10.13" macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64" - name: "macOS 10.15 x86_64"
os: macos-13 platform: macos
os: macos-15-intel
cibw_arch: x86_64 cibw_arch: x86_64
build: "pp3*" build: "{cp314,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-14
cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator"
platform: ios
os: macos-15-intel
cibw_arch: x86_64_iphonesimulator
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
submodules: true submodules: true
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"
@ -103,6 +124,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
@ -110,12 +132,11 @@ jobs:
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_SKIP: pp39-*
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- 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:
@ -133,18 +154,18 @@ jobs:
- cibw_arch: ARM64 - cibw_arch: ARM64
os: windows-11-arm os: windows-11-arm
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
path: Tests\test-images path: Tests\test-images
- uses: actions/setup-python@v5 - uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"
@ -160,7 +181,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
@ -188,7 +209,6 @@ jobs:
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw" CIBW_CACHE_PATH: "C:\\cibw"
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow -v {project}:C:\pillow
@ -212,15 +232,15 @@ jobs:
path: winbuild\build\bin\fribidi* path: winbuild\build\bin\fribidi*
sdist: sdist:
if: github.event_name != 'schedule' if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: "3.x" python-version: "3.x"
@ -231,15 +251,33 @@ jobs:
name: dist-sdist name: dist-sdist
path: dist/*.tar.gz path: dist/*.tar.gz
count-dists:
needs: [build-native-wheels, windows, sdist]
runs-on: ubuntu-latest
name: Count dists
steps:
- uses: actions/download-artifact@v5
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: "What did we get?"
run: |
ls -alR
echo "Number of dists, should be $EXPECTED_DISTS:"
files=$(ls dist 2>/dev/null | wc -l)
echo $files
[ "$files" -eq $EXPECTED_DISTS ] || exit 1
scientific-python-nightly-wheels-publish: scientific-python-nightly-wheels-publish:
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [build-native-wheels, windows] needs: count-dists
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels name: Upload wheels to scientific-python-nightly-wheels
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v5
with: with:
pattern: dist-* pattern: dist-!(sdist)*
path: dist path: dist
merge-multiple: true merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels - name: Upload wheels to scientific-python-nightly-wheels
@ -250,7 +288,7 @@ jobs:
pypi-publish: pypi-publish:
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: [build-native-wheels, windows, sdist] needs: count-dists
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Upload release to PyPI name: Upload release to PyPI
environment: environment:
@ -259,7 +297,7 @@ jobs:
permissions: permissions:
id-token: write id-token: write
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v5
with: with:
pattern: dist-* pattern: dist-*
path: dist path: dist

2
.github/zizmor.yml vendored
View File

@ -1,5 +1,5 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI # Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/ # https://docs.zizmor.sh/configuration/
rules: rules:
unpinned-uses: unpinned-uses:
config: config:

View File

@ -1,17 +1,17 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8 rev: v0.13.3
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
rev: 25.1.0 rev: 25.9.0
hooks: hooks:
- 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.3 rev: v21.1.2
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -36,7 +36,7 @@ repos:
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v6.0.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable - id: check-shebang-scripts-are-executable
@ -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.34.0
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/zizmorcore/zizmor-pre-commit
rev: v1.6.0 rev: v1.14.2
hooks: hooks:
- id: zizmor - id: zizmor
@ -68,7 +68,7 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.1 rev: v2.7.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
@ -79,7 +79,7 @@ repos:
additional_dependencies: [trove-classifiers>=2024.10.12] additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt - repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.5.0 rev: 1.6.0
hooks: hooks:
- id: tox-ini-fmt - id: tox-ini-fmt

View File

@ -13,6 +13,9 @@ include LICENSE
include Makefile include Makefile
include tox.ini include tox.ini
graft Tests graft Tests
graft Tests/images
graft checks
graft patches
graft src graft src
graft depends graft depends
graft winbuild graft winbuild
@ -26,8 +29,19 @@ exclude .editorconfig
exclude .readthedocs.yml exclude .readthedocs.yml
exclude codecov.yml exclude codecov.yml
exclude renovate.json exclude renovate.json
exclude Tests/images/README.md
exclude Tests/images/crash*.tif
exclude Tests/images/string_dimension.tiff
global-exclude .git* global-exclude .git*
global-exclude *.pyc global-exclude *.pyc
global-exclude *.so global-exclude *.so
prune .ci prune .ci
prune wheels prune wheels
prune winbuild/build
prune winbuild/depends
prune Tests/errors
prune Tests/images/jpeg2000
prune Tests/images/msp
prune Tests/images/picins
prune Tests/images/sunraster
prune Tests/test-images

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
@ -97,13 +97,27 @@ test:
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
python3 -m pytest -qq python3 -m pytest -qq
.PHONY: test-p
test-p:
python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist
python3 -m pytest -qq -n auto
.PHONY: valgrind .PHONY: valgrind
valgrind: valgrind:
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
--log-file=/tmp/valgrind-output \ --log-file=/tmp/valgrind-output \
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: valgrind-leak
valgrind-leak:
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \
--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \
--log-file=/tmp/valgrind-output \
python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: readme .PHONY: readme
readme: readme:
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2

View File

@ -36,9 +36,6 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
alt="GitHub Actions build status (Test MinGW)" alt="GitHub Actions build status (Test MinGW)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml"><img
alt="GitHub Actions build status (Test Cygwin)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
alt="GitHub Actions build status (Test Docker)" alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>

1
Tests/createfontdatachunk.py Executable file → Normal file
View File

@ -1,4 +1,3 @@
#!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import base64 import base64

View File

@ -10,17 +10,20 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from collections.abc import Sequence
from functools import lru_cache from functools import lru_cache
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import Any, Callable
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
from PIL import Image, ImageFile, ImageMath, features from PIL import Image, ImageFile, ImageMath, features
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
uploader = None uploader = None
@ -172,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
return pytest.mark.skipif(not features.check(feature), reason=reason) return pytest.mark.skipif(not features.check(feature), reason=reason)
def has_feature_version(feature: str, required: str) -> bool:
version = features.version(feature)
assert version is not None
version_required = parse_version(required)
version_available = parse_version(version)
return version_available >= version_required
def skip_unless_feature_version( def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator: ) -> pytest.MarkDecorator:
@ -271,17 +282,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
@ -295,16 +302,6 @@ def djpeg_available() -> bool:
return False return False
def cjpeg_available() -> bool:
if shutil.which("cjpeg"):
try:
subprocess.check_call(["cjpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
return False
def netpbm_available() -> bool: def netpbm_available() -> bool:
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
Tests/images/frame_size.mpo Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

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

BIN
Tests/images/op_index.qoi Normal file

Binary file not shown.

BIN
Tests/images/p_4_planes.pcx Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

View File

@ -14,3 +14,23 @@
fun:_TIFFReadEncodedTileAndAllocBuffer fun:_TIFFReadEncodedTileAndAllocBuffer
... ...
} }
{
<python_alloc_possible_leak>
Memcheck:Leak
match-leak-kinds: all
fun:malloc
fun:_PyMem_RawMalloc
fun:PyObject_Malloc
...
}
{
<python_realloc_possible_leak>
Memcheck:Leak
match-leak-kinds: all
fun:malloc
fun:_PyMem_RawRealloc
fun:PyMem_Realloc
...
}

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)

275
Tests/test_arro3.py Normal file
View File

@ -0,0 +1,275 @@
from __future__ import annotations
import json
from typing import Any, NamedTuple
import pytest
from PIL import Image
from .helper import (
assert_deep_equal,
assert_image_equal,
hopper,
is_big_endian,
)
TYPE_CHECKING = False
if TYPE_CHECKING:
from arro3 import compute
from arro3.core import (
Array,
DataType,
Field,
fixed_size_list_array,
)
else:
arro3 = pytest.importorskip("arro3", reason="Arro3 not installed")
from arro3 import compute
from arro3.core import Array, DataType, Field, fixed_size_list_array
TEST_IMAGE_SIZE = (10, 10)
def _test_img_equals_pyarray(
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
) -> None:
assert img.height * img.width * elts_per_pixel == len(arr)
px = img.load()
assert px is not None
if elts_per_pixel > 1 and mask is None:
# have to do element-wise comparison when we're comparing
# flattened r,g,b,a to a pixel.
mask = list(range(elts_per_pixel))
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
if mask:
pixel = px[x, y]
assert isinstance(pixel, tuple)
for ix, elt in enumerate(mask):
if elts_per_pixel == 1:
assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
else:
assert (
pixel[ix]
== arr[(y * img.width + x) * elts_per_pixel + elt].as_py()
)
else:
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
def _test_img_equals_int32_pyarray(
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
) -> None:
assert img.height * img.width * elts_per_pixel == len(arr)
px = img.load()
assert px is not None
if mask is None:
# have to do element-wise comparison when we're comparing
# flattened rgba in an uint32 to a pixel.
mask = list(range(elts_per_pixel))
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
pixel = px[x, y]
assert isinstance(pixel, tuple)
arr_pixel_int = arr[y * img.width + x].as_py()
arr_pixel_tuple = (
arr_pixel_int % 256,
(arr_pixel_int // 256) % 256,
(arr_pixel_int // 256**2) % 256,
(arr_pixel_int // 256**3),
)
if is_big_endian():
arr_pixel_tuple = arr_pixel_tuple[::-1]
for ix, elt in enumerate(mask):
assert pixel[ix] == arr_pixel_tuple[elt]
fl_uint8_4_type = DataType.list(Field("_", DataType.uint8()).with_nullable(False), 4)
@pytest.mark.parametrize(
"mode, dtype, mask",
(
("L", DataType.uint8(), None),
("I", DataType.int32(), None),
("F", DataType.float32(), None),
("LA", fl_uint8_4_type, [0, 3]),
("RGB", fl_uint8_4_type, [0, 1, 2]),
("RGBA", fl_uint8_4_type, None),
("RGBX", fl_uint8_4_type, None),
("CMYK", fl_uint8_4_type, None),
("YCbCr", fl_uint8_4_type, [0, 1, 2]),
("HSV", fl_uint8_4_type, [0, 1, 2]),
),
)
def test_to_array(mode: str, dtype: DataType, mask: list[int] | None) -> None:
img = hopper(mode)
# Resize to non-square
img = img.crop((3, 0, 124, 127))
assert img.size == (121, 127)
arr = Array(img)
_test_img_equals_pyarray(img, arr, mask)
assert arr.type == dtype
reloaded = Image.fromarrow(arr, mode, img.size)
assert_image_equal(img, reloaded)
def test_lifetime() -> None:
# valgrind shouldn't error out here.
# arrays should be accessible after the image is deleted.
img = hopper("L")
arr_1 = Array(img)
arr_2 = Array(img)
del img
assert compute.sum(arr_1).as_py() > 0
del arr_1
assert compute.sum(arr_2).as_py() > 0
del arr_2
def test_lifetime2() -> None:
# valgrind shouldn't error out here.
# img should remain after the arrays are collected.
img = hopper("L")
arr_1 = Array(img)
arr_2 = Array(img)
assert compute.sum(arr_1).as_py() > 0
del arr_1
assert compute.sum(arr_2).as_py() > 0
del arr_2
img2 = img.copy()
px = img2.load()
assert px # make mypy happy
assert isinstance(px[0, 0], int)
class DataShape(NamedTuple):
dtype: DataType
# Strictly speaking, elt should be a pixel or pixel component, so
# list[uint8][4], float, int, uint32, uint8, etc. But more
# correctly, it should be exactly the dtype from the line above.
elt: Any
elts_per_pixel: int
UINT_ARR = DataShape(
dtype=fl_uint8_4_type,
elt=[1, 2, 3, 4], # array of 4 uint8 per pixel
elts_per_pixel=1, # only one array per pixel
)
UINT = DataShape(
dtype=DataType.uint8(),
elt=3, # one uint8,
elts_per_pixel=4, # but repeated 4x per pixel
)
UINT32 = DataShape(
dtype=DataType.uint32(),
elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000
elts_per_pixel=1, # one per pixel
)
INT32 = DataShape(
dtype=DataType.uint32(),
elt=0x12CDEF45, # one packed int
elts_per_pixel=1, # one per pixel
)
@pytest.mark.parametrize(
"mode, data_tp, mask",
(
("L", DataShape(DataType.uint8(), 3, 1), None),
("I", DataShape(DataType.int32(), 1 << 24, 1), None),
("F", DataShape(DataType.float32(), 3.14159, 1), None),
("LA", UINT_ARR, [0, 3]),
("LA", UINT, [0, 3]),
("RGB", UINT_ARR, [0, 1, 2]),
("RGBA", UINT_ARR, None),
("CMYK", UINT_ARR, None),
("YCbCr", UINT_ARR, [0, 1, 2]),
("HSV", UINT_ARR, [0, 1, 2]),
("RGB", UINT, [0, 1, 2]),
("RGBA", UINT, None),
("CMYK", UINT, None),
("YCbCr", UINT, [0, 1, 2]),
("HSV", UINT, [0, 1, 2]),
),
)
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
if dtype == fl_uint8_4_type:
tmp_arr = Array(elt * (ct_pixels * elts_per_pixel), type=DataType.uint8())
arr = fixed_size_list_array(tmp_arr, 4)
else:
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
_test_img_equals_pyarray(img, arr, mask, elts_per_pixel)
@pytest.mark.parametrize(
"mode, mask",
(
("LA", [0, 3]),
("RGB", [0, 1, 2]),
("RGBA", None),
("CMYK", None),
("YCbCr", [0, 1, 2]),
("HSV", [0, 1, 2]),
),
)
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
(dtype, elt, elts_per_pixel) = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
_test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)
@pytest.mark.parametrize(
"mode, metadata",
(
("LA", ["L", "X", "X", "A"]),
("RGB", ["R", "G", "B", "X"]),
("RGBX", ["R", "G", "B", "X"]),
("RGBA", ["R", "G", "B", "A"]),
("CMYK", ["C", "M", "Y", "K"]),
("YCbCr", ["Y", "Cb", "Cr", "X"]),
("HSV", ["H", "S", "V", "X"]),
),
)
def test_image_metadata(mode: str, metadata: list[str]) -> None:
img = hopper(mode)
arr = Array(img)
assert arr.type.value_field
assert arr.type.value_field.metadata
assert arr.type.value_field.metadata[b"image"]
parsed_metadata = json.loads(arr.type.value_field.metadata[b"image"].decode("utf8"))
assert "bands" in parsed_metadata
assert parsed_metadata["bands"] == metadata

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,
@ -47,25 +47,24 @@ def test_unknown_version() -> None:
], ],
) )
def test_old_version(deprecated: str, plural: bool, expected: str) -> None: def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
expected = r""
with pytest.raises(RuntimeError, match=expected): with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural) _deprecate.deprecate(deprecated, 1, plural=plural)
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"
) )
@ -78,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

@ -2,7 +2,6 @@ from __future__ import annotations
import io import io
import re import re
from typing import Callable
import pytest import pytest
@ -10,6 +9,10 @@ from PIL import features
from .helper import skip_unless_feature from .helper import skip_unless_feature
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
def test_check() -> None: def test_check() -> None:
# Check the correctness of the convenience function # Check the correctness of the convenience function
@ -18,11 +21,7 @@ def test_check() -> None:
for codec in features.codecs: for codec in features.codecs:
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: assert features.check_feature(feature) == features.check(feature)
with pytest.warns(DeprecationWarning):
assert features.check_feature(feature) == features.check(feature)
else:
assert features.check_feature(feature) == features.check(feature)
def test_version() -> None: def test_version() -> None:
@ -48,26 +47,7 @@ def test_version() -> None:
for codec in features.codecs: for codec in features.codecs:
test(codec, features.version_codec) test(codec, features.version_codec)
for feature in features.features: for feature in features.features:
if "webp" in feature: test(feature, features.version_feature)
with pytest.warns(DeprecationWarning):
test(feature, features.version_feature)
else:
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")
@ -95,10 +75,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:
@ -128,6 +107,25 @@ def test_unsupported_module() -> None:
features.version_module(module) features.version_module(module)
def test_unsupported_feature() -> None:
# Arrange
feature = "unsupported_feature"
# Act / Assert
with pytest.raises(ValueError):
features.check_feature(feature)
with pytest.raises(ValueError):
features.version_feature(feature)
def test_unsupported_version() -> None:
assert features.version("unsupported_version") is None
def test_modulenotfound(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(features, "features", {"test": ("PIL._test", "", "")})
assert features.check_feature("test") is None
@pytest.mark.parametrize("supported_formats", (True, False)) @pytest.mark.parametrize("supported_formats", (True, False))
def test_pilinfo(supported_formats: bool) -> None: def test_pilinfo(supported_formats: bool) -> None:
buf = io.StringIO() buf = io.StringIO()

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

@ -14,6 +14,7 @@ import pytest
from PIL import ( from PIL import (
AvifImagePlugin, AvifImagePlugin,
GifImagePlugin,
Image, Image,
ImageDraw, ImageDraw,
ImageFile, ImageFile,
@ -77,8 +78,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
@ -220,6 +221,7 @@ class TestFileAvif:
def test_background_from_gif(self, tmp_path: Path) -> None: def test_background_from_gif(self, tmp_path: Path) -> None:
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 AVIF # Save as AVIF
out_avif = tmp_path / "temp.avif" out_avif = tmp_path / "temp.avif"
@ -232,6 +234,7 @@ class TestFileAvif:
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 <= 6 assert difference <= 6
@ -240,6 +243,7 @@ class TestFileAvif:
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
im.save(temp_file) im.save(temp_file)
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
def test_invalid_file(self) -> None: def test_invalid_file(self) -> None:
@ -254,7 +258,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))
@ -596,10 +602,12 @@ class TestAvifAnimation:
""" """
with Image.open(TEST_AVIF_FILE) as im: with Image.open(TEST_AVIF_FILE) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
with Image.open("Tests/images/avif/star.avifs") as im: with Image.open("Tests/images/avif/star.avifs") as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 5 assert im.n_frames == 5
assert im.is_animated assert im.is_animated
@ -610,11 +618,13 @@ class TestAvifAnimation:
""" """
with Image.open("Tests/images/avif/star.gif") as original: with Image.open("Tests/images/avif/star.gif") as original:
assert isinstance(original, GifImagePlugin.GifImageFile)
assert original.n_frames > 1 assert original.n_frames > 1
temp_file = tmp_path / "temp.avif" temp_file = tmp_path / "temp.avif"
original.save(temp_file, save_all=True) original.save(temp_file, save_all=True)
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == original.n_frames assert im.n_frames == original.n_frames
# Compare first frame in P mode to frame from original GIF # Compare first frame in P mode to frame from original GIF
@ -634,6 +644,7 @@ class TestAvifAnimation:
def check(temp_file: Path) -> None: def check(temp_file: Path) -> None:
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 4 assert im.n_frames == 4
# Compare first frame to original # Compare first frame to original
@ -706,6 +717,7 @@ class TestAvifAnimation:
) )
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 5 assert im.n_frames == 5
assert im.is_animated assert im.is_animated
@ -735,6 +747,7 @@ class TestAvifAnimation:
) )
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 5 assert im.n_frames == 5
assert im.is_animated assert im.is_animated

View File

@ -7,9 +7,8 @@ import pytest
from PIL import BlpImagePlugin, Image from PIL import BlpImagePlugin, Image
from .helper import ( from .helper import (
assert_image_equal,
assert_image_equal_tofile, assert_image_equal_tofile,
assert_image_similar, assert_image_similar_tofile,
hopper, hopper,
) )
@ -52,18 +51,16 @@ def test_save(tmp_path: Path) -> None:
im = hopper("P") im = hopper("P")
im.save(f, blp_version=version) im.save(f, blp_version=version)
with Image.open(f) as reloaded: assert_image_equal_tofile(im.convert("RGB"), f)
assert_image_equal(im.convert("RGB"), reloaded)
with Image.open("Tests/images/transparent.png") as im: with Image.open("Tests/images/transparent.png") as im:
f = tmp_path / "temp.blp" f = tmp_path / "temp.blp"
im.convert("P").save(f, blp_version=version) im.convert("P").save(f, blp_version=version)
with Image.open(f) as reloaded: assert_image_similar_tofile(im, f, 8)
assert_image_similar(im, reloaded, 8)
im = hopper() im = hopper()
with pytest.raises(ValueError): with pytest.raises(ValueError, match="Unsupported BLP image mode"):
im.save(f) im.save(f)

View File

@ -6,6 +6,8 @@ from pathlib import Path
import pytest import pytest
from PIL import BmpImagePlugin, Image, _binary from PIL import BmpImagePlugin, Image, _binary
from PIL._binary import o16le as o16
from PIL._binary import o32le as o32
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -114,7 +116,7 @@ def test_save_float_dpi(tmp_path: Path) -> None:
def test_load_dib() -> None: def test_load_dib() -> None:
# test for #1293, Imagegrab returning Unsupported Bitfields Format # test for #1293, ImageGrab returning Unsupported Bitfields Format
with Image.open("Tests/images/clipboard.dib") as im: with Image.open("Tests/images/clipboard.dib") as im:
assert im.format == "DIB" assert im.format == "DIB"
assert im.get_format_mimetype() == "image/bmp" assert im.get_format_mimetype() == "image/bmp"
@ -219,6 +221,18 @@ def test_rle8_eof(file_name: str, length: int) -> None:
im.load() im.load()
def test_unsupported_bmp_bitfields_layout() -> None:
fp = io.BytesIO(
o32(40) # header size
+ b"\x00" * 10
+ o16(1) # bits
+ o32(3) # BITFIELDS compression
+ b"\x00" * 32
)
with pytest.raises(OSError, match="Unsupported BMP bitfields layout"):
Image.open(fp)
def test_offset() -> None: def test_offset() -> None:
# This image has been hexedited # This image has been hexedited
# to exclude the palette size from the pixel data offset # to exclude the palette size from the pixel data offset

View File

@ -5,6 +5,9 @@ from io import BytesIO
import pytest import pytest
from PIL import CurImagePlugin, Image from PIL import CurImagePlugin, Image
from PIL._binary import o8
from PIL._binary import o16le as o16
from PIL._binary import o32le as o32
from .helper import assert_image_equal from .helper import assert_image_equal
@ -21,6 +24,24 @@ def test_sanity() -> None:
assert im.getpixel((16, 16)) == (84, 87, 86, 255) assert im.getpixel((16, 16)) == (84, 87, 86, 255)
def test_largest_cursor() -> None:
magic = b"\x00\x00\x02\x00"
sizes = ((1, 1), (8, 8), (4, 4))
data = magic + o16(len(sizes))
for w, h in sizes:
image_offset = 6 + len(sizes) * 16 if (w, h) == max(sizes) else 0
data += o8(w) + o8(h) + o8(0) * 10 + o32(image_offset)
data += (
o32(12) # header size
+ o16(8) # width
+ o16(16) # height
+ o16(0) # planes
+ o16(1) # bits
)
with Image.open(BytesIO(data)) as im:
assert im.size == (8, 8)
def test_posy_link() -> None: def test_posy_link() -> None:
with Image.open("Tests/images/cur/posy_link.cur") as im: with Image.open("Tests/images/cur/posy_link.cur") as im:
assert im.size == (128, 128) assert im.size == (128, 128)
@ -66,6 +87,7 @@ def test_invalid_file() -> None:
no_cursors_file = "Tests/images/cur/no_cursors.cur" no_cursors_file = "Tests/images/cur/no_cursors.cur"
cur = CurImagePlugin.CurImageFile("Tests/images/cur/deerstalker.cur") cur = CurImagePlugin.CurImageFile("Tests/images/cur/deerstalker.cur")
assert cur.fp is not None
cur.fp.close() cur.fp.close()
with open(no_cursors_file, "rb") as cur.fp: with open(no_cursors_file, "rb") as cur.fp:
with pytest.raises(TypeError): with pytest.raises(TypeError):

View File

@ -380,21 +380,28 @@ def test_palette() -> None:
assert_image_equal_tofile(im, "Tests/images/transparent.gif") assert_image_equal_tofile(im, "Tests/images/transparent.gif")
def test_unsupported_header_size() -> None:
with pytest.raises(OSError, match="Unsupported header size 0"):
with Image.open(BytesIO(b"DDS " + b"\x00" * 4)):
pass
def test_unsupported_bitcount() -> None: def test_unsupported_bitcount() -> None:
with pytest.raises(OSError): with pytest.raises(OSError, match="Unsupported bitcount 24 for 131072"):
with Image.open("Tests/images/unsupported_bitcount.dds"): with Image.open("Tests/images/unsupported_bitcount.dds"):
pass pass
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file, message",
( (
"Tests/images/unimplemented_dxgi_format.dds", ("Tests/images/unimplemented_dxgi_format.dds", "Unimplemented DXGI format 93"),
"Tests/images/unimplemented_pfflags.dds", ("Tests/images/unimplemented_pixel_format.dds", "Unimplemented pixel format 0"),
("Tests/images/unimplemented_pfflags.dds", "Unknown pixel format flags 8"),
), ),
) )
def test_not_implemented(test_file: str) -> None: def test_not_implemented(test_file: str, message: str) -> None:
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError, match=message):
with Image.open(test_file): with Image.open(test_file):
pass pass
@ -511,3 +518,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

@ -197,6 +197,14 @@ def test_load_long_binary_data(prefix: bytes) -> None:
assert img.format == "EPS" assert img.format == "EPS"
def test_begin_binary() -> None:
with open("Tests/images/eps/binary_preview_map.eps", "rb") as fp:
data = bytearray(fp.read())
data[76875 : 76875 + 11] = b"%" * 11
with Image.open(io.BytesIO(data)) as img:
assert img.size == (399, 480)
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )

View File

@ -48,6 +48,7 @@ def test_sanity() -> None:
def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None: def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open(animated_test_file_with_prefix_chunk) as im: with Image.open(animated_test_file_with_prefix_chunk) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.mode == "P" assert im.mode == "P"
assert im.size == (320, 200) assert im.size == (320, 200)
assert im.format == "FLI" assert im.format == "FLI"
@ -55,6 +56,7 @@ def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
assert im.is_animated assert im.is_animated
palette = im.getpalette() palette = im.getpalette()
assert palette is not None
assert palette[3:6] == [255, 255, 255] assert palette[3:6] == [255, 255, 255]
assert palette[381:384] == [204, 204, 12] assert palette[381:384] == [204, 204, 12]
assert palette[765:] == [252, 0, 0] assert palette[765:] == [252, 0, 0]

View File

@ -1,8 +1,10 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
import pytest import pytest
from PIL import GbrImagePlugin, Image from PIL import GbrImagePlugin, Image, _binary
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
@ -31,8 +33,49 @@ def test_multiple_load_operations() -> None:
assert_image_equal_tofile(im, "Tests/images/gbr.png") assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_invalid_file() -> None: def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
invalid_file = "Tests/images/flower.jpg" return BytesIO(
b"".join(
_binary.o32be(i)
for i in [
info.get("header_size", 20),
info.get("version", 1),
info.get("width", 1),
info.get("height", 1),
info.get("color_depth", 1),
]
)
+ magic_number
)
with pytest.raises(SyntaxError):
def test_invalid_file() -> None:
for f in [
create_gbr_image({"header_size": 0}),
create_gbr_image({"width": 0}),
create_gbr_image({"height": 0}),
]:
with pytest.raises(SyntaxError, match="not a GIMP brush"):
GbrImagePlugin.GbrImageFile(f)
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError, match="Unsupported GIMP brush version"):
GbrImagePlugin.GbrImageFile(invalid_file) GbrImagePlugin.GbrImageFile(invalid_file)
def test_unsupported_gimp_brush() -> None:
f = create_gbr_image({"color_depth": 2})
with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"):
GbrImagePlugin.GbrImageFile(f)
def test_bad_magic_number() -> None:
f = create_gbr_image({"version": 2}, magic_number=b"badm")
with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"):
GbrImagePlugin.GbrImageFile(f)
def test_L() -> None:
f = create_gbr_image()
with Image.open(f) as im:
assert im.mode == "L"

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 GdImageFile, UnidentifiedImageError from PIL import GdImageFile, UnidentifiedImageError
@ -16,6 +18,14 @@ def test_sanity() -> None:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14) assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14)
def test_transparency() -> None:
with open(TEST_GD_FILE, "rb") as fp:
data = bytearray(fp.read())
data[7:11] = b"\x00\x00\x00\x05"
with GdImageFile.open(BytesIO(data)) as im:
assert im.info["transparency"] == 5
def test_bad_mode() -> None: def test_bad_mode() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
GdImageFile.open(TEST_GD_FILE, "bad mode") GdImageFile.open(TEST_GD_FILE, "bad mode")

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
@ -280,6 +293,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
im.save(out, save_all=True) im.save(out, save_all=True)
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 5 assert reread.n_frames == 5
@ -540,7 +554,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 +1245,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 +1269,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:
@ -1357,8 +1375,10 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
with Image.open(out) as im: with Image.open(out) as im:
# 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 isinstance(im, GifImagePlugin.GifImageFile)
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 +1442,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 +1499,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,28 +1,87 @@
from __future__ import annotations from __future__ import annotations
import sys from io import BytesIO
from io import BytesIO, StringIO
import pytest import pytest
from PIL import Image, IptcImagePlugin from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/iptc.jpg" TEST_FILE = "Tests/images/iptc.jpg"
def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
def field(tag, value):
return bytes((0x1C,) + tag + (0, len(value))) + value
data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
data += field((3, 120), bytes((info.get("compression", 1),)))
if "band" in info:
data += field((3, 65), bytes((info["band"] + 1,)))
data += field((3, 20), b"\x01") # width
data += field((3, 30), b"\x01") # height
data += field(
(8, 10),
bytes((info.get("data", 0),)),
)
return BytesIO(data)
def test_open() -> None: def test_open() -> None:
expected = Image.new("L", (1, 1)) expected = Image.new("L", (1, 1))
f = BytesIO( f = create_iptc_image()
b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
)
with Image.open(f) as im: with Image.open(f) as im:
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] assert im.tile == [("iptc", (0, 0, 1, 1), 25, ("raw", None))]
assert_image_equal(im, expected) assert_image_equal(im, expected)
with Image.open(f) as im:
assert im.load() is not None
def test_field_length() -> None:
f = create_iptc_image()
f.seek(28)
f.write(b"\xff")
with pytest.raises(OSError, match="illegal field length in IPTC/NAA file"):
with Image.open(f):
pass
@pytest.mark.parametrize("layers, mode", ((3, "RGB"), (4, "CMYK")))
def test_layers(layers: int, mode: str) -> None:
for band in range(-1, layers):
info = {"layers": layers, "component": 1, "data": 5}
if band != -1:
info["band"] = band
f = create_iptc_image(info)
with Image.open(f) as im:
assert im.mode == mode
data = [0] * layers
data[max(band, 0)] = 5
assert im.getpixel((0, 0)) == tuple(data)
def test_unknown_compression() -> None:
f = create_iptc_image({"compression": 2})
with pytest.raises(OSError, match="Unknown IPTC image compression"):
with Image.open(f):
pass
def test_getiptcinfo() -> None:
f = create_iptc_image()
with Image.open(f) as im:
assert IptcImagePlugin.getiptcinfo(im) == {
(3, 60): b"\x01\x00",
(3, 120): b"\x01",
(3, 20): b"\x01",
(3, 30): b"\x01",
}
def test_getiptcinfo_jpg_none() -> None: def test_getiptcinfo_jpg_none() -> None:
# Arrange # Arrange
@ -75,13 +134,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 +157,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

@ -26,7 +26,6 @@ from .helper import (
assert_image_equal_tofile, assert_image_equal_tofile,
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
cjpeg_available,
djpeg_available, djpeg_available,
hopper, hopper,
is_win32, is_win32,
@ -130,30 +129,26 @@ 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: cmyk = im.getpixel((0, 0))
# the source image has red pixels in the upper left corner. assert isinstance(cmyk, tuple)
c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) c, m, y, k = (x / 255.0 for x in cmyk)
assert c == 0.0 assert c == 0.0
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 # the opposite corner is black
c, m, y, k = ( cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1))
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) assert isinstance(cmyk, tuple)
) 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 # roundtrip, and check again
im = self.roundtrip(im) check(self.roundtrip(im))
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
c, m, y, k = (
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
)
assert k > 0.9
def test_rgb(self) -> None: def test_rgb(self) -> None:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]: def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]:
@ -335,8 +330,10 @@ class TestFileJpeg:
# Reading # Reading
with Image.open("Tests/images/exif_gps.jpg") as im: with Image.open("Tests/images/exif_gps.jpg") as im:
exif = im._getexif() assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert exif[gps_index] == expected_exif_gps exif_data = im._getexif()
assert exif_data is not None
assert exif_data[gps_index] == expected_exif_gps
# Writing # Writing
f = tmp_path / "temp.jpg" f = tmp_path / "temp.jpg"
@ -345,8 +342,10 @@ class TestFileJpeg:
hopper().save(f, exif=exif) hopper().save(f, exif=exif)
with Image.open(f) as reloaded: with Image.open(f) as reloaded:
exif = reloaded._getexif() assert isinstance(reloaded, JpegImagePlugin.JpegImageFile)
assert exif[gps_index] == expected_exif_gps exif_data = reloaded._getexif()
assert exif_data is not None
assert exif_data[gps_index] == expected_exif_gps
def test_empty_exif_gps(self) -> None: def test_empty_exif_gps(self) -> None:
with Image.open("Tests/images/empty_gps_ifd.jpg") as im: with Image.open("Tests/images/empty_gps_ifd.jpg") as im:
@ -373,6 +372,7 @@ class TestFileJpeg:
exifs = [] exifs = []
for i in range(2): for i in range(2):
with Image.open("Tests/images/exif-200dpcm.jpg") as im: with Image.open("Tests/images/exif-200dpcm.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
exifs.append(im._getexif()) exifs.append(im._getexif())
assert exifs[0] == exifs[1] assert exifs[0] == exifs[1]
@ -406,13 +406,17 @@ class TestFileJpeg:
} }
with Image.open("Tests/images/exif_gps.jpg") as im: with Image.open("Tests/images/exif_gps.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
exif = im._getexif() exif = im._getexif()
assert exif is not None
for tag, value in expected_exif.items(): for tag, value in expected_exif.items():
assert value == exif[tag] assert value == exif[tag]
def test_exif_gps_typeerror(self) -> None: def test_exif_gps_typeerror(self) -> None:
with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
# Should not raise a TypeError # Should not raise a TypeError
im._getexif() im._getexif()
@ -492,7 +496,9 @@ class TestFileJpeg:
def test_exif(self) -> None: def test_exif(self) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im: with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
info = im._getexif() info = im._getexif()
assert info is not None
assert info[305] == "Adobe Photoshop CS Macintosh" assert info[305] == "Adobe Photoshop CS Macintosh"
def test_get_child_images(self) -> None: def test_get_child_images(self) -> None:
@ -615,6 +621,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,
@ -677,11 +701,13 @@ class TestFileJpeg:
def test_save_multiple_16bit_qtables(self) -> None: def test_save_multiple_16bit_qtables(self) -> None:
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
im2 = self.roundtrip(im, qtables="keep") im2 = self.roundtrip(im, qtables="keep")
assert im.quantization == im2.quantization assert im.quantization == im2.quantization
def test_save_single_16bit_qtable(self) -> None: def test_save_single_16bit_qtable(self) -> None:
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) im2 = self.roundtrip(im, qtables={0: im.quantization[0]})
assert len(im2.quantization) == 1 assert len(im2.quantization) == 1
assert im2.quantization[0] == im.quantization[0] assert im2.quantization[0] == im.quantization[0]
@ -717,14 +743,6 @@ class TestFileJpeg:
img.load_djpeg() img.load_djpeg()
assert_image_similar_tofile(img, TEST_FILE, 5) assert_image_similar_tofile(img, TEST_FILE, 5)
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
def test_save_cjpeg(self, tmp_path: Path) -> None:
with Image.open(TEST_FILE) as img:
tempfile = str(tmp_path / "temp.jpg")
JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17)
def test_no_duplicate_0x1001_tag(self) -> None: def test_no_duplicate_0x1001_tag(self) -> None:
# Arrange # Arrange
tag_ids = {v: k for k, v in ExifTags.TAGS.items()} tag_ids = {v: k for k, v in ExifTags.TAGS.items()}
@ -750,10 +768,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:
@ -890,7 +911,10 @@ class TestFileJpeg:
# in contrast to normal 8 # in contrast to normal 8
with Image.open("Tests/images/exif-ifd-offset.jpg") as im: with Image.open("Tests/images/exif-ifd-offset.jpg") as im:
# Act / Assert # Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09" assert isinstance(im, JpegImagePlugin.JpegImageFile)
exif = im._getexif()
assert exif is not None
assert exif[306] == "2017:03:13 23:03:09"
def test_multiple_exif(self) -> None: def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im: with Image.open("Tests/images/multiple_exif.jpg") as im:
@ -1065,10 +1089,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]
@ -1092,14 +1122,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:
@ -367,6 +355,27 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault # Should not segfault
im.save(outfile) im.save(outfile)
def test_whitepoint_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = tmp_path / "temp.tif"
hopper().save(out, tiffinfo={318: (0.3127, 0.3289)})
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289))
# Save tag by default
out = tmp_path / "temp2.tif"
with Image.open("Tests/images/rdf.tif") as im:
im.save(out)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289999))
def test_xmlpacket_tag( def test_xmlpacket_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None: ) -> None:
@ -377,8 +386,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 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag"
assert reloaded.tag_v2[700] == b"xmlpacket tag"
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
# issue #1765 # issue #1765
@ -724,8 +732,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 +784,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:
@ -898,8 +893,8 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (128, 128) assert im.size == (128, 128)
assert im.format == "TIFF" assert im.format == "TIFF"
im2 = hopper() with hopper() as im2:
assert_image_similar(im, im2, 5) assert_image_similar(im, im2, 5)
except OSError: except OSError:
captured = capfd.readouterr() captured = capfd.readouterr()
if "LZMA compression support is not configured" in captured.err: if "LZMA compression support is not configured" in captured.err:

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

@ -104,25 +104,27 @@ def test_exif(test_file: str) -> None:
def test_frame_size() -> None: def test_frame_size() -> None:
# This image has been hexedited to contain a different size with Image.open("Tests/images/frame_size.mpo") as im:
# in the SOF marker of the second frame assert im.size == (56, 70)
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: im.load()
assert im.size == (640, 480)
im.seek(1) im.seek(1)
assert im.size == (680, 480) assert im.size == (349, 434)
im.load()
im.seek(0) im.seek(0)
assert im.size == (640, 480) assert im.size == (56, 70)
def test_ignore_frame_size() -> None: def test_ignore_frame_size() -> None:
# Ignore the different size of the second frame # Ignore the different size of the second frame
# since this is not a "Large Thumbnail" image # since this is not a "Large Thumbnail" image
with Image.open("Tests/images/ignore_frame_size.mpo") as im: with Image.open("Tests/images/ignore_frame_size.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.size == (64, 64) assert im.size == (64, 64)
im.seek(1) im.seek(1)
assert im.mpinfo is not None
assert ( assert (
im.mpinfo[0xB002][1]["Attribute"]["MPType"] im.mpinfo[0xB002][1]["Attribute"]["MPType"]
== "Multi-Frame Image: (Disparity)" == "Multi-Frame Image: (Disparity)"
@ -155,7 +157,9 @@ def test_reload_exif_after_seek() -> None:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
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:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
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
@ -164,7 +168,9 @@ def test_mp_offset() -> None:
# This image has been manually hexedited to have an IFD offset of 10 # This image has been manually hexedited to have an IFD offset of 10
# 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:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
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
@ -180,7 +186,9 @@ def test_mp_no_data() -> None:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
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:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
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 +301,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 +322,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

@ -1,10 +1,17 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image from PIL import Image
from .helper import assert_image_equal
def test_load_raw() -> None: def test_load_raw() -> None:
with Image.open("Tests/images/hopper.pcd") as im: with Image.open("Tests/images/hopper.pcd") as im:
assert im.size == (768, 512)
im.load() # should not segfault. im.load() # should not segfault.
# Note that this image was created with a resized hopper # Note that this image was created with a resized hopper
@ -15,3 +22,18 @@ def test_load_raw() -> None:
# target = hopper().resize((768,512)) # target = hopper().resize((768,512))
# assert_image_similar(im, target, 10) # assert_image_similar(im, target, 10)
@pytest.mark.parametrize("orientation", (1, 3))
def test_rotated(orientation: int) -> None:
with open("Tests/images/hopper.pcd", "rb") as fp:
data = bytearray(fp.read())
data[2048 + 1538] = orientation
f = BytesIO(data)
with Image.open(f) as im:
assert im.size == (512, 768)
with Image.open("Tests/images/hopper.pcd") as expected:
assert_image_equal(
im, expected.rotate(90 if orientation == 1 else 270, expand=True)
)

View File

@ -37,6 +37,11 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f) im.save(f)
def test_p_4_planes() -> None:
with Image.open("Tests/images/p_4_planes.pcx") as im:
assert im.getpixel((0, 0)) == 3
def test_bad_image_size() -> None: def test_bad_image_size() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp: with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read() data = fp.read()

View File

@ -100,11 +100,11 @@ class TestFilePng:
assert im.format == "PNG" assert im.format == "PNG"
assert im.get_format_mimetype() == "image/png" assert im.get_format_mimetype() == "image/png"
for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]:
im = hopper(mode) im = hopper(mode)
im.save(test_file) im.save(test_file)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
if mode in ("I", "I;16B"): if mode == "I;16B":
reloaded = reloaded.convert(mode) reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im) assert_image_equal(reloaded, im)
@ -229,7 +229,9 @@ class TestFilePng:
assert_image(im, "RGBA", (162, 150)) assert_image(im, "RGBA", (162, 150))
# image has 124 unique alpha values # image has 124 unique alpha values
assert len(im.getchannel("A").getcolors()) == 124 colors = im.getchannel("A").getcolors()
assert colors is not None
assert len(colors) == 124
def test_load_transparent_rgb(self) -> None: def test_load_transparent_rgb(self) -> None:
test_file = "Tests/images/rgb_trns.png" test_file = "Tests/images/rgb_trns.png"
@ -241,7 +243,9 @@ class TestFilePng:
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][0] == 876 colors = im.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == 876
def test_save_p_transparent_palette(self, tmp_path: Path) -> None: def test_save_p_transparent_palette(self, tmp_path: Path) -> None:
in_file = "Tests/images/pil123p.png" in_file = "Tests/images/pil123p.png"
@ -262,7 +266,9 @@ class TestFilePng:
assert_image(im, "RGBA", (162, 150)) assert_image(im, "RGBA", (162, 150))
# image has 124 unique alpha values # image has 124 unique alpha values
assert len(im.getchannel("A").getcolors()) == 124 colors = im.getchannel("A").getcolors()
assert colors is not None
assert len(colors) == 124
def test_save_p_single_transparency(self, tmp_path: Path) -> None: def test_save_p_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/p_trns_single.png" in_file = "Tests/images/p_trns_single.png"
@ -285,7 +291,9 @@ class TestFilePng:
assert im.getpixel((31, 31)) == (0, 255, 52, 0) assert im.getpixel((31, 31)) == (0, 255, 52, 0)
# image has 876 transparent pixels # image has 876 transparent pixels
assert im.getchannel("A").getcolors()[0][0] == 876 colors = im.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == 876
def test_save_p_transparent_black(self, tmp_path: Path) -> None: def test_save_p_transparent_black(self, tmp_path: Path) -> None:
# check if solid black image with full transparency # check if solid black image with full transparency
@ -313,7 +321,9 @@ class TestFilePng:
assert im.info["transparency"] == 255 assert im.info["transparency"] == 255
im_rgba = im.convert("RGBA") im_rgba = im.convert("RGBA")
assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == num_transparent
test_file = tmp_path / "temp.png" test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
@ -324,7 +334,9 @@ class TestFilePng:
assert_image_equal(im, test_im) assert_image_equal(im, test_im)
test_im_rgba = test_im.convert("RGBA") test_im_rgba = test_im.convert("RGBA")
assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent colors = test_im_rgba.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == num_transparent
def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/caption_6_33_22.png" in_file = "Tests/images/caption_6_33_22.png"
@ -671,6 +683,9 @@ class TestFilePng:
im.save(out, bits=4, save_all=save_all) im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
assert reloaded.png is not None
assert reloaded.png.im_palette is not None
assert len(reloaded.png.im_palette[1]) == 48 assert len(reloaded.png.im_palette[1]) == 48
def test_plte_length(self, tmp_path: Path) -> None: def test_plte_length(self, tmp_path: Path) -> None:
@ -681,6 +696,9 @@ class TestFilePng:
im.save(out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
assert reloaded.png is not None
assert reloaded.png.im_palette is not None
assert len(reloaded.png.im_palette[1]) == 3 assert len(reloaded.png.im_palette[1]) == 3
def test_getxmp(self) -> None: def test_getxmp(self) -> None:
@ -702,13 +720,17 @@ class TestFilePng:
def test_exif(self) -> None: def test_exif(self) -> None:
# With an EXIF chunk # With an EXIF chunk
with Image.open("Tests/images/exif.png") as im: with Image.open("Tests/images/exif.png") as im:
exif = im._getexif() assert isinstance(im, PngImagePlugin.PngImageFile)
assert exif[274] == 1 exif_data = im._getexif()
assert exif_data is not None
assert exif_data[274] == 1
# With an ImageMagick zTXt chunk # With an ImageMagick zTXt chunk
with Image.open("Tests/images/exif_imagemagick.png") as im: with Image.open("Tests/images/exif_imagemagick.png") as im:
exif = im._getexif() assert isinstance(im, PngImagePlugin.PngImageFile)
assert exif[274] == 1 exif_data = im._getexif()
assert exif_data is not None
assert exif_data[274] == 1
# Assert that info still can be extracted # Assert that info still can be extracted
# when the image is no longer a PngImageFile instance # when the image is no longer a PngImageFile instance
@ -717,8 +739,10 @@ class TestFilePng:
# With a tEXt chunk # With a tEXt chunk
with Image.open("Tests/images/exif_text.png") as im: with Image.open("Tests/images/exif_text.png") as im:
exif = im._getexif() assert isinstance(im, PngImagePlugin.PngImageFile)
assert exif[274] == 1 exif_data = im._getexif()
assert exif_data is not None
assert exif_data[274] == 1
# With XMP tags # With XMP tags
with Image.open("Tests/images/xmp_tags_orientation.png") as im: with Image.open("Tests/images/xmp_tags_orientation.png") as im:
@ -740,8 +764,10 @@ class TestFilePng:
im.save(test_file, exif=im.getexif()) im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
exif = reloaded._getexif() assert isinstance(reloaded, PngImagePlugin.PngImageFile)
assert exif[274] == 1 exif_data = reloaded._getexif()
assert exif_data is not None
assert exif_data[274] == 1
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@ -801,6 +827,16 @@ class TestFilePng:
with Image.open("Tests/images/truncated_end_chunk.png") as im: with Image.open("Tests/images/truncated_end_chunk.png") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.png") assert_image_equal_tofile(im, "Tests/images/hopper.png")
def test_deprecation(self, tmp_path: Path) -> None:
test_file = tmp_path / "out.png"
im = hopper("I")
with pytest.warns(DeprecationWarning, match="Saving I mode images as PNG"):
im.save(test_file)
with Image.open(test_file) as reloaded:
assert_image_equal(im, reloaded.convert("I"))
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@skip_unless_feature("zlib") @skip_unless_feature("zlib")

View File

@ -92,6 +92,13 @@ def test_16bit_pgm() -> None:
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff") assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff")
def test_p4_save(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper_1bit.pbm") as im:
filename = tmp_path / "temp.pbm"
im.save(filename)
assert_image_equal_tofile(im, filename)
def test_16bit_pgm_write(tmp_path: Path) -> None: def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im: with Image.open("Tests/images/16_bit_binary.pgm") as im:
filename = tmp_path / "temp.pgm" filename = tmp_path / "temp.pgm"
@ -134,6 +141,12 @@ def test_pfm_big_endian(tmp_path: Path) -> None:
assert_image_equal_tofile(im, filename) assert_image_equal_tofile(im, filename)
def test_save_unsupported_mode(tmp_path: Path) -> None:
im = hopper("P")
with pytest.raises(OSError, match="cannot write mode P as PPM"):
im.save(tmp_path / "out.ppm")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"data", "data",
[ [
@ -288,14 +301,16 @@ def test_non_integer_token(tmp_path: Path) -> None:
pass pass
def test_header_token_too_long(tmp_path: Path) -> None: @pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890"))
def test_header_token_too_long(tmp_path: Path, data: bytes) -> None:
path = tmp_path / "temp.ppm" path = tmp_path / "temp.ppm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P6\n 01234567890") f.write(data)
with pytest.raises(ValueError, match="Token too long in file header: 01234567890"): with pytest.raises(ValueError) as e:
with Image.open(path): with Image.open(path):
pass pass
assert "Token too long in file header: " in repr(e)
def test_truncated_file(tmp_path: Path) -> None: def test_truncated_file(tmp_path: Path) -> None:

View File

@ -1,10 +1,12 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import Image, QoiImagePlugin from PIL import Image, QoiImagePlugin
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile, hopper
def test_sanity() -> None: def test_sanity() -> None:
@ -28,3 +30,28 @@ def test_invalid_file() -> None:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
QoiImagePlugin.QoiImageFile(invalid_file) QoiImagePlugin.QoiImageFile(invalid_file)
def test_op_index() -> None:
# QOI_OP_INDEX as the first chunk
with Image.open("Tests/images/op_index.qoi") as im:
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
def test_save(tmp_path: Path) -> None:
f = tmp_path / "temp.qoi"
im = hopper()
im.save(f, colorspace="sRGB")
assert_image_equal_tofile(im, f)
for path in ("Tests/images/default_font.png", "Tests/images/pil123rgba.png"):
with Image.open(path) as im:
im.save(f)
assert_image_equal_tofile(im, f)
im = hopper("P")
with pytest.raises(ValueError, match="Unsupported QOI image mode"):
im.save(f)

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:
@ -268,13 +274,17 @@ def test_save_l_transparency(tmp_path: Path) -> None:
in_file = "Tests/images/la.tga" in_file = "Tests/images/la.tga"
with Image.open(in_file) as im: with Image.open(in_file) as im:
assert im.mode == "LA" assert im.mode == "LA"
assert im.getchannel("A").getcolors()[0][0] == num_transparent colors = im.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == num_transparent
out = tmp_path / "temp.tga" out = tmp_path / "temp.tga"
im.save(out) im.save(out)
with Image.open(out) as test_im: with Image.open(out) as test_im:
assert test_im.mode == "LA" assert test_im.mode == "LA"
assert test_im.getchannel("A").getcolors()[0][0] == num_transparent colors = test_im.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == num_transparent
assert_image_equal(im, test_im) assert_image_equal(im, test_im)

View File

@ -14,6 +14,7 @@ from PIL import (
ImageFile, ImageFile,
JpegImagePlugin, JpegImagePlugin,
TiffImagePlugin, TiffImagePlugin,
TiffTags,
UnidentifiedImageError, UnidentifiedImageError,
) )
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
@ -48,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:
@ -235,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:
@ -695,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"
@ -900,6 +891,29 @@ class TestFileTiff:
assert description[0]["format"] == "image/tiff" assert description[0]["format"] == "image/tiff"
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
def test_getxmp_undefined(self, tmp_path: Path) -> None:
tmpfile = tmp_path / "temp.tif"
im = Image.new("L", (1, 1))
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd.tagtype[700] = TiffTags.UNDEFINED
with Image.open("Tests/images/lab.tif") as im_xmp:
ifd[700] = im_xmp.info["xmp"]
im.save(tmpfile, tiffinfo=ifd)
with Image.open(tmpfile) as im_reloaded:
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im_reloaded.getxmp() == {}
else:
assert "xmp" in im_reloaded.info
xmp = im_reloaded.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
assert description[0]["format"] == "image/tiff"
def test_get_photoshop_blocks(self) -> None: def test_get_photoshop_blocks(self) -> None:
with Image.open("Tests/images/lab.tif") as im: with Image.open("Tests/images/lab.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile) assert isinstance(im, TiffImagePlugin.TiffImageFile)
@ -1005,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

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from PIL import WalImageFile from PIL import WalImageFile
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
@ -13,12 +15,22 @@ def test_open() -> None:
assert im.format_description == "Quake2 Texture" assert im.format_description == "Quake2 Texture"
assert im.mode == "P" assert im.mode == "P"
assert im.size == (128, 128) assert im.size == (128, 128)
assert "next_name" not in im.info
assert isinstance(im, WalImageFile.WalImageFile) assert isinstance(im, WalImageFile.WalImageFile)
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
def test_next_name() -> None:
with open(TEST_FILE, "rb") as fp:
data = bytearray(fp.read())
data[56:60] = b"Test"
f = BytesIO(data)
with WalImageFile.open(f) as im:
assert im.info["next_name"] == b"Test"
def test_load() -> None: def test_load() -> None:
with WalImageFile.open(TEST_FILE) as im: with WalImageFile.open(TEST_FILE) as im:
px = im.load() px = im.load()

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

@ -4,13 +4,13 @@ from collections.abc import Generator
from pathlib import Path from pathlib import Path
import pytest import pytest
from packaging.version import parse as parse_version
from PIL import GifImagePlugin, Image, WebPImagePlugin, features from PIL import GifImagePlugin, Image, WebPImagePlugin
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
assert_image_similar, assert_image_similar,
has_feature_version,
is_big_endian, is_big_endian,
skip_unless_feature, skip_unless_feature,
) )
@ -53,11 +53,8 @@ def test_write_animation_L(tmp_path: Path) -> None:
im.load() im.load()
assert_image_similar(im, orig.convert("RGBA"), 32.9) assert_image_similar(im, orig.convert("RGBA"), 32.9)
if is_big_endian(): if is_big_endian() and not has_feature_version("webp", "1.2.2"):
version = features.version_module("webp") pytest.skip("Fails with libwebp earlier than 1.2.2")
assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
orig.seek(orig.n_frames - 1) orig.seek(orig.n_frames - 1)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
orig.load() orig.load()
@ -81,11 +78,8 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
assert_image_equal(im, frame1.convert("RGBA")) assert_image_equal(im, frame1.convert("RGBA"))
# Compare second frame to original # Compare second frame to original
if is_big_endian(): if is_big_endian() and not has_feature_version("webp", "1.2.2"):
version = features.version_module("webp") pytest.skip("Fails with libwebp earlier than 1.2.2")
assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
im.seek(1) im.seek(1)
im.load() im.load()
assert_image_equal(im, frame2.convert("RGBA")) assert_image_equal(im, frame2.convert("RGBA"))

View File

@ -22,11 +22,13 @@ except ImportError:
def test_read_exif_metadata() -> None: def test_read_exif_metadata() -> None:
file_path = "Tests/images/flower.webp" file_path = "Tests/images/flower.webp"
with Image.open(file_path) as image: with Image.open(file_path) as image:
assert isinstance(image, WebPImagePlugin.WebPImageFile)
assert image.format == "WEBP" assert image.format == "WEBP"
exif_data = image.info.get("exif", None) exif_data = image.info.get("exif", None)
assert exif_data assert exif_data
exif = image._getexif() exif = image._getexif()
assert exif is not None
# Camera make # Camera make
assert exif[271] == "Canon" assert exif[271] == "Canon"

View File

@ -44,6 +44,18 @@ def test_load_zero_inch() -> None:
pass pass
def test_load_unsupported_wmf() -> None:
b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x01" * 10)
with pytest.raises(SyntaxError, match="Unsupported WMF file format"):
WmfImagePlugin.WmfStubImageFile(b)
def test_load_unsupported() -> None:
b = BytesIO(b"\x01\x00\x00\x00")
with pytest.raises(SyntaxError, match="Unsupported file format"):
WmfImagePlugin.WmfStubImageFile(b)
def test_render() -> None: def test_render() -> None:
with open("Tests/images/drawing.emf", "rb") as fp: with open("Tests/images/drawing.emf", "rb") as fp:
data = fp.read() data = fp.read()

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

@ -9,7 +9,8 @@ from .helper import skip_unless_feature
class TestFontCrash: class TestFontCrash:
def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
# from fuzzers.fuzz_font # Copy of the code from fuzz_font() in Tests/oss-fuzz/fuzzers.py
# that triggered a problem when fuzzing
font.getbbox("ABC") font.getbbox("ABC")
font.getmask("test text") font.getmask("test text")
with Image.new(mode="RGBA", size=(200, 200)) as im: with Image.new(mode="RGBA", size=(200, 200)) as im:

View File

@ -2,12 +2,15 @@ from __future__ import annotations
import colorsys import colorsys
import itertools import itertools
from typing import Callable
from PIL import Image from PIL import Image
from .helper import assert_image_similar, hopper from .helper import assert_image_similar, hopper
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
def int_to_float(i: int) -> float: def int_to_float(i: int) -> float:
return i / 255 return i / 255

View File

@ -19,6 +19,7 @@ from PIL import (
ImageDraw, ImageDraw,
ImageFile, ImageFile,
ImagePalette, ImagePalette,
ImageShow,
UnidentifiedImageError, UnidentifiedImageError,
features, features,
) )
@ -30,7 +31,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 +50,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 +132,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 +151,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):
@ -289,33 +284,6 @@ class TestImage:
assert item is not None assert item is not None
assert item != num assert item != num
def test_expand_x(self) -> None:
# Arrange
im = hopper()
orig_size = im.size
xmargin = 5
# Act
im = im._expand(xmargin)
# Assert
assert im.size[0] == orig_size[0] + 2 * xmargin
assert im.size[1] == orig_size[1] + 2 * xmargin
def test_expand_xy(self) -> None:
# Arrange
im = hopper()
orig_size = im.size
xmargin = 5
ymargin = 3
# Act
im = im._expand(xmargin, ymargin)
# Assert
assert im.size[0] == orig_size[0] + 2 * xmargin
assert im.size[1] == orig_size[1] + 2 * ymargin
def test_getbands(self) -> None: def test_getbands(self) -> None:
# Assert # Assert
assert hopper("RGB").getbands() == ("R", "G", "B") assert hopper("RGB").getbands() == ("R", "G", "B")
@ -394,6 +362,37 @@ class TestImage:
assert img_colors is not None assert img_colors is not None
assert sorted(img_colors) == expected_colors assert sorted(img_colors) == expected_colors
def test_alpha_composite_la(self) -> None:
# Arrange
expected_colors = sorted(
[
(3300, (255, 255)),
(1156, (170, 192)),
(1122, (128, 255)),
(1089, (0, 0)),
(1122, (255, 128)),
(1122, (0, 128)),
(1089, (0, 255)),
]
)
dst = Image.new("LA", size=(100, 100), color=(0, 255))
draw = ImageDraw.Draw(dst)
draw.rectangle((0, 33, 100, 66), fill=(0, 128))
draw.rectangle((0, 67, 100, 100), fill=(0, 0))
src = Image.new("LA", size=(100, 100), color=(255, 255))
draw = ImageDraw.Draw(src)
draw.rectangle((33, 0, 66, 100), fill=(255, 128))
draw.rectangle((67, 0, 100, 100), fill=(255, 0))
# Act
img = Image.alpha_composite(dst, src)
# Assert
img_colors = img.getcolors()
assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_inplace(self) -> None: def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue") src = Image.new("RGBA", (128, 128), "blue")
@ -671,6 +670,7 @@ class TestImage:
im_remapped = im.remap_palette(list(range(256))) im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im, im_remapped) assert_image_equal(im, im_remapped)
assert im.palette is not None assert im.palette is not None
assert im_remapped.palette is not None
assert im.palette.palette == im_remapped.palette.palette assert im.palette.palette == im_remapped.palette.palette
# Test illegal image mode # Test illegal image mode
@ -927,6 +927,17 @@ class TestImage:
reloaded_exif.load(exif.tobytes()) reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
def test_delete_ifd_tag(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
exif.get_ifd(0x8769)
assert 0x8769 in exif
del exif[0x8769]
reloaded_exif = Image.Exif()
reloaded_exif.load(exif.tobytes())
assert 0x8769 not in reloaded_exif
def test_exif_load_from_fp(self) -> None: def test_exif_load_from_fp(self) -> None:
with Image.open("Tests/images/flower.jpg") as im: with Image.open("Tests/images/flower.jpg") as im:
data = im.info["exif"] data = im.info["exif"]
@ -973,6 +984,11 @@ class TestImage:
assert tag not in exif.get_ifd(0x8769) assert tag not in exif.get_ifd(0x8769)
assert exif.get_ifd(0xA005) assert exif.get_ifd(0xA005)
def test_exif_from_xmp_bytes(self) -> None:
im = Image.new("RGB", (1, 1))
im.info["xmp"] = b'\xff tiff:Orientation="2"'
assert im.getexif()[274] == 2
def test_empty_xmp(self) -> None: def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im: with Image.open("Tests/images/hopper.gif") as im:
if ElementTree is None: if ElementTree is None:
@ -989,7 +1005,7 @@ class TestImage:
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
im.info["xmp"] = ( im.info["xmp"] = (
b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n' b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00' b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00 '
) )
if ElementTree is None: if ElementTree is None:
with pytest.warns( with pytest.warns(
@ -1002,9 +1018,16 @@ 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() == []
def test_show(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageShow, "_viewers", [])
im = Image.new("RGB", (1, 1))
with pytest.warns(DeprecationWarning, match="Image._show"):
Image._show(im)
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None: def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size) im = Image.new("RGB", size)
@ -1076,6 +1099,12 @@ class TestImage:
assert im.palette is not None assert im.palette is not None
assert im.palette.colors[(27, 35, 6, 214)] == 24 assert im.palette.colors[(27, 35, 6, 214)] == 24
def test_merge_pa(self) -> None:
p = hopper("P")
a = Image.new("L", p.size)
pa = Image.merge("PA", (p, a))
assert p.getpalette() == pa.getpalette()
def test_constants(self) -> None: def test_constants(self) -> None:
for enum in ( for enum in (
Image.Transpose, Image.Transpose,
@ -1132,39 +1161,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),
@ -329,3 +315,10 @@ int main(int argc, char* argv[])
process = subprocess.Popen(["embed_pil.exe"], env=env) process = subprocess.Popen(["embed_pil.exe"], env=env)
process.communicate() process.communicate()
assert process.returncode == 0 assert process.returncode == 0
def teardown_method(self) -> None:
try:
os.remove("embed_pil.c")
except FileNotFoundError:
# If the test was skipped or failed, the file won't exist
pass

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)
@ -100,7 +101,7 @@ def test_fromarray_strides_without_tobytes() -> None:
self.__array_interface__ = arr_params self.__array_interface__ = arr_params
with pytest.raises(ValueError): with pytest.raises(ValueError):
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)}) wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1), "typestr": "|u1"})
Image.fromarray(wrapped, "L") Image.fromarray(wrapped, "L")
@ -115,3 +116,11 @@ def test_fromarray_palette() -> None:
# 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
assert len(out.palette.colors) == len(out.im.getpalette()) / 3 assert len(out.palette.colors) == len(out.im.getpalette()) / 3
def test_deprecation() -> None:
a = numpy.array(im.convert("L"))
with pytest.warns(
DeprecationWarning, match="'mode' parameter for changing data types"
):
Image.fromarray(a, "1")

View File

@ -97,6 +97,13 @@ def test_opaque() -> None:
assert_image_equal(alpha, solid) assert_image_equal(alpha, solid)
def test_rgba() -> None:
with Image.open("Tests/images/transparent.png") as im:
assert im.mode == "RGBA"
assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5)
def test_rgba_p() -> None: def test_rgba_p() -> None:
im = hopper("RGBA") im = hopper("RGBA")
im.putalpha(hopper("L")) im.putalpha(hopper("L"))
@ -107,11 +114,19 @@ def test_rgba_p() -> None:
assert_image_similar(im, comparable, 20) assert_image_similar(im, comparable, 20)
def test_rgba() -> None: def test_rgba_pa() -> None:
with Image.open("Tests/images/transparent.png") as im: im = hopper("RGBA").convert("PA").convert("RGB")
assert im.mode == "RGBA" expected = hopper("RGB")
assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) assert_image_similar(im, expected, 9.3)
def test_pa() -> None:
im = hopper().convert("PA")
palette = im.palette
assert palette is not None
assert palette.colors != {}
def test_trns_p(tmp_path: Path) -> None: def test_trns_p(tmp_path: Path) -> None:
@ -203,7 +218,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

@ -10,9 +10,12 @@ def test_histogram() -> None:
assert histogram("1") == (256, 0, 10994) assert histogram("1") == (256, 0, 10994)
assert histogram("L") == (256, 0, 662) assert histogram("L") == (256, 0, 662)
assert histogram("LA") == (512, 0, 16384)
assert histogram("La") == (512, 0, 16384)
assert histogram("I") == (256, 0, 662) assert histogram("I") == (256, 0, 662)
assert histogram("F") == (256, 0, 662) assert histogram("F") == (256, 0, 662)
assert histogram("P") == (256, 0, 1551) assert histogram("P") == (256, 0, 1551)
assert histogram("PA") == (512, 0, 16384)
assert histogram("RGB") == (768, 4, 675) assert histogram("RGB") == (768, 4, 675)
assert histogram("RGBA") == (1024, 0, 16384) assert histogram("RGBA") == (1024, 0, 16384)
assert histogram("CMYK") == (1024, 0, 16384) assert histogram("CMYK") == (1024, 0, 16384)

View File

@ -124,6 +124,21 @@ class TestImagingPaste:
im = im.crop((12, 23, im2.width + 12, im2.height + 23)) im = im.crop((12, 23, im2.width + 12, im2.height + 23))
assert_image_equal(im, im2) assert_image_equal(im, im2)
@pytest.mark.parametrize("y", [10, -10])
@pytest.mark.parametrize("mode", ["L", "RGB"])
@pytest.mark.parametrize("mask_mode", ["", "1", "L", "LA", "RGBa"])
def test_image_self(self, y: int, mode: str, mask_mode: str) -> None:
im = getattr(self, "gradient_" + mode)
mask = Image.new(mask_mode, im.size, 0xFFFFFFFF) if mask_mode else None
im_self = im.copy()
im_self.paste(im_self, (0, y), mask)
im_copy = im.copy()
im_copy.paste(im_copy.copy(), (0, y), mask)
assert_image_equal(im_self, im_copy)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_1(self, mode: str) -> None: def test_image_mask_1(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")

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

@ -62,6 +62,7 @@ def test_putpalette_with_alpha_values() -> None:
expected = im.convert("RGBA") expected = im.convert("RGBA")
palette = im.getpalette() palette = im.getpalette()
assert palette is not None
transparency = im.info.pop("transparency") transparency = im.info.pop("transparency")
palette_with_alpha_values = [] palette_with_alpha_values = []

View File

@ -1,11 +1,16 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from packaging.version import parse as parse_version
from PIL import Image, features from PIL import Image
from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature from .helper import (
assert_image_similar,
has_feature_version,
hopper,
is_ppc64le,
skip_unless_feature,
)
def test_sanity() -> None: def test_sanity() -> None:
@ -23,11 +28,8 @@ def test_sanity() -> None:
@skip_unless_feature("libimagequant") @skip_unless_feature("libimagequant")
def test_libimagequant_quantize() -> None: def test_libimagequant_quantize() -> None:
image = hopper() image = hopper()
if is_ppc64le(): if is_ppc64le() and not has_feature_version("libimagequant", "4"):
version = features.version_feature("libimagequant") pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
assert version is not None
if parse_version(version) < parse_version("4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P" assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15) assert_image_similar(converted.convert("RGB"), image, 15)
@ -70,6 +72,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
@ -115,6 +118,15 @@ def test_quantize_kmeans(method: Image.Quantize) -> None:
im.quantize(kmeans=-1, method=method) im.quantize(kmeans=-1, method=method)
@skip_unless_feature("libimagequant")
def test_resize() -> None:
im = hopper().resize((100, 100))
converted = im.quantize(100, Image.Quantize.LIBIMAGEQUANT)
colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_colors() -> None: def test_colors() -> None:
im = hopper() im = hopper()
colors = 2 colors = 2

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

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import math import math
from typing import Callable
import pytest import pytest
@ -9,6 +8,10 @@ from PIL import Image, ImageTransform
from .helper import assert_image_equal, assert_image_similar, hopper from .helper import assert_image_equal, assert_image_similar, hopper
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
class TestImageTransform: class TestImageTransform:
def test_sanity(self) -> None: def test_sanity(self) -> None:
@ -48,6 +51,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:

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