Merge branch 'main' into ttb

This commit is contained in:
Andrew Murray 2025-06-28 13:49:43 +10:00
commit 26ae44e059
376 changed files with 20644 additions and 3287 deletions

View File

@ -2,12 +2,12 @@
aptget_update() aptget_update()
{ {
if [ ! -z $1 ]; then if [ -n "$1" ]; then
echo "" echo ""
echo "Retrying apt-get update..." echo "Retrying apt-get update..."
echo "" echo ""
fi fi
output=`sudo apt-get update 2>&1` output=$(sudo apt-get update 2>&1)
echo "$output" echo "$output"
if [[ $output == *[WE]:\ * ]]; then if [[ $output == *[WE]:\ * ]]; then
return 1 return 1
@ -20,10 +20,10 @@ fi
set -e set -e
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-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 sway wl-clipboard libopenblas-dev nasm
fi fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
@ -36,6 +36,9 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
# optional test dependency, only install if there's a binary package.
# fails on beta 3.14 and PyPy
python3 -m pip install --only-binary=:all: pyarrow || true
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install numpy python3 -m pip install numpy
@ -50,7 +53,7 @@ if [[ $(uname) != CYGWIN* ]]; then
# Pyroma uses non-isolated build and fails with old setuptools # Pyroma uses non-isolated build and fails with old setuptools
if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
# To match pyproject.toml # To match pyproject.toml
python3 -m pip install "setuptools>=67.8" python3 -m pip install "setuptools>=77"
fi fi
# webp # webp
@ -62,6 +65,9 @@ if [[ $(uname) != CYGWIN* ]]; then
# raqm # raqm
pushd depends && ./install_raqm.sh && popd pushd depends && ./install_raqm.sh && popd
# libavif
pushd depends && ./install_libavif.sh && popd
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd
else else

View File

@ -1 +1 @@
cibuildwheel==2.22.0 cibuildwheel==3.0.0

View File

@ -1,9 +1,10 @@
mypy==1.14.1 mypy==1.16.1
IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6 IceSpringPySideStubs-PySide6
ipython ipython
numpy numpy
packaging packaging
pyarrow-stubs
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

@ -1,5 +1,26 @@
# A clang-format style that approximates Python's PEP 7 # A clang-format style that approximates Python's PEP 7
# Useful for IDE integration # Useful for IDE integration
Language: C
BasedOnStyle: Google
AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false
AlignAfterOpenBracket: BlockIndent
BinPackArguments: false
BinPackParameters: false
BreakBeforeBraces: Attach
ColumnLimit: 88
DerivePointerAlignment: false
IndentGotoLabels: false
IndentWidth: 4
PointerAlignment: Right
ReflowComments: true
SortIncludes: false
SpaceBeforeParens: ControlStatements
SpacesInParentheses: false
TabWidth: 4
UseTab: Never
---
Language: Cpp
BasedOnStyle: Google BasedOnStyle: Google
AlwaysBreakAfterReturnType: All AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false
@ -11,7 +32,6 @@ ColumnLimit: 88
DerivePointerAlignment: false DerivePointerAlignment: false
IndentGotoLabels: false IndentGotoLabels: false
IndentWidth: 4 IndentWidth: 4
Language: Cpp
PointerAlignment: Right PointerAlignment: Right
ReflowComments: true ReflowComments: true
SortIncludes: false SortIncludes: false

46
.github/ISSUE_TEMPLATE/RELEASE.md vendored Normal file
View File

@ -0,0 +1,46 @@
---
name: "Maintainers only: Release"
about: For maintainers to schedule a quarterly release
labels: Release
---
## Main release
Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch.
* [ ] Add release notes e.g. https://github.com/python-pillow/Pillow/pull/8885
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch.
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
* [ ] Create branch and tag for release e.g.:
```bash
git branch [[MAJOR.MINOR]].x
git tag [[MAJOR.MINOR]].0
git push --tags
```
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases).
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
```bash
git push --all
```
## Publicize release
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
## Documentation
* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes
## Docker images
* [ ] Update Pillow in the Docker Images repository
```bash
git clone https://github.com/python-pillow/docker-images
cd docker-images
./update-pillow-tag.sh [[release tag]]
```

View File

@ -16,6 +16,6 @@
} }
], ],
"schedule": [ "schedule": [
"on the 3rd day of the month" "* * 3 * *"
] ]
} }

View File

@ -6,6 +6,8 @@ if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven brew uninstall gradle maven
fi fi
brew install \ brew install \
aom \
dav1d \
freetype \ freetype \
ghostscript \ ghostscript \
jpeg-turbo \ jpeg-turbo \
@ -14,6 +16,8 @@ brew install \
libtiff \ libtiff \
little-cms2 \ little-cms2 \
openjpeg \ openjpeg \
rav1e \
svt-av1 \
webp webp
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
@ -26,6 +30,12 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install numpy python3 -m pip install numpy
# optional test dependency, only install if there's a binary package.
# fails on beta 3.14 and PyPy
python3 -m pip install --only-binary=:all: pyarrow || true
# libavif
pushd depends && ./install_libavif.sh && popd
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -35,6 +35,10 @@ jobs:
matrix: matrix:
os: ["ubuntu-latest"] os: ["ubuntu-latest"]
docker: [ docker: [
# Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-24.04-noble-ppc64le,
ubuntu-24.04-noble-s390x,
# Then run the remainder
alpine, alpine,
amazon-2-amd64, amazon-2-amd64,
amazon-2023-amd64, amazon-2023-amd64,
@ -43,8 +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-40-amd64,
fedora-41-amd64, fedora-41-amd64,
fedora-42-amd64,
gentoo, gentoo,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64, ubuntu-24.04-noble-amd64,
@ -52,13 +56,9 @@ jobs:
dockerTag: [main] dockerTag: [main]
include: include:
- docker: "ubuntu-24.04-noble-ppc64le" - docker: "ubuntu-24.04-noble-ppc64le"
os: "ubuntu-22.04"
qemu-arch: "ppc64le" qemu-arch: "ppc64le"
dockerTag: main
- docker: "ubuntu-24.04-noble-s390x" - docker: "ubuntu-24.04-noble-s390x"
os: "ubuntu-22.04"
qemu-arch: "s390x" qemu-arch: "s390x"
dockerTag: main
- docker: "ubuntu-24.04-noble-arm64v8" - docker: "ubuntu-24.04-noble-arm64v8"
os: "ubuntu-24.04-arm" os: "ubuntu-24.04-arm"
dockerTag: main dockerTag: main
@ -75,8 +75,9 @@ jobs:
- name: Set up QEMU - name: Set up QEMU
if: "matrix.qemu-arch" if: "matrix.qemu-arch"
run: | uses: docker/setup-qemu-action@v3
docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }} with:
platforms: ${{ matrix.qemu-arch }}
- name: Docker pull - name: Docker pull
run: | run: |

View File

@ -60,6 +60,8 @@ jobs:
mingw-w64-x86_64-gcc \ mingw-w64-x86_64-gcc \
mingw-w64-x86_64-ghostscript \ mingw-w64-x86_64-ghostscript \
mingw-w64-x86_64-lcms2 \ mingw-w64-x86_64-lcms2 \
mingw-w64-x86_64-libavif \
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libjpeg-turbo \
mingw-w64-x86_64-libraqm \ mingw-w64-x86_64-libraqm \
mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libtiff \

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

@ -31,18 +31,17 @@ 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.10", "3.10", "3.11", "3.12", "3.13", "3.14"] python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "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.9", architecture: "x86" }
timeout-minutes: 30 timeout-minutes: 45
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
@ -84,18 +83,22 @@ jobs:
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
- name: Install CPython dependencies - name: Install CPython dependencies
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" if: "!contains(matrix.python-version, 'pypy') && !contains(matrix.python-version, '3.14') && matrix.architecture != 'x86'"
run: | run: |
python3 -m pip install PyQt6 python3 -m pip install PyQt6
- name: Install PyArrow dependency
run: |
python3 -m pip install --only-binary=:all: pyarrow || true
- name: Install dependencies - name: Install dependencies
id: install id: install
run: | run: |
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.4.0 --no-progress choco install ghostscript --version=10.5.1 --no-progress
echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH echo "C:\Program Files\gs\gs10.05.1\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
@ -145,6 +148,10 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libpng.cmd" run: "& winbuild\\build\\build_dep_libpng.cmd"
- name: Build dependencies / libavif
if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64'
run: "& winbuild\\build\\build_dep_libavif.cmd"
# for FreeType WOFF2 font support # for FreeType WOFF2 font support
- name: Build dependencies / brotli - name: Build dependencies / brotli
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'

View File

@ -41,7 +41,9 @@ jobs:
"ubuntu-latest", "ubuntu-latest",
] ]
python-version: [ python-version: [
"pypy3.11",
"pypy3.10", "pypy3.10",
"3.14t",
"3.14", "3.14",
"3.13t", "3.13t",
"3.13", "3.13",
@ -54,6 +56,7 @@ jobs:
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 } - { python-version: "3.10", 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+ # M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" } - { os: "macos-13", python-version: "3.9" }
@ -69,7 +72,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: Quansight-Labs/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true

View File

@ -25,7 +25,7 @@ else
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 PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
# Define custom utilities # Define custom utilities
source wheels/multibuild/common_utils.sh source wheels/multibuild/common_utils.sh
@ -38,40 +38,43 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds # Package versions for fresh source builds
FREETYPE_VERSION=2.13.3 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.2.0 HARFBUZZ_VERSION=11.2.1
LIBPNG_VERSION=1.6.46 LIBPNG_VERSION=1.6.49
JPEGTURBO_VERSION=3.1.0 JPEGTURBO_VERSION=3.1.1
OPENJPEG_VERSION=2.5.3 OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.4 XZ_VERSION=5.8.1
TIFF_VERSION=4.6.0 TIFF_VERSION=4.7.0
LCMS2_VERSION=2.16 LCMS2_VERSION=2.17
ZLIB_NG_VERSION=2.2.3 ZLIB_VERSION=1.3.1
ZLIB_NG_VERSION=2.2.4
LIBWEBP_VERSION=1.5.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
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
ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
CFLAGS="$CFLAGS -Wno-int-conversion"
build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
--disable-debug --disable-host-tool --with-internal-glib \ --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
CFLAGS=$ORIGINAL_CFLAGS
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
fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat
(cd zlib-ng-$ZLIB_NG_VERSION \
&& ./configure --prefix=$BUILD_PREFIX --zlib-compat \ if [ -n "$IS_MACOS" ]; then
&& make -j4 \ # Ensure that on macOS, the library name is an absolute path, not an
&& make install) # @rpath, so that delocate picks up the right library (and doesn't need
# DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an
# option to control the install_name.
install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
fi
touch zlib-stamp touch zlib-stamp
} }
@ -90,18 +93,75 @@ 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=release -Dfreetype=enabled -Dglib=disabled) && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled)
(cd $out_dir/build \ (cd $out_dir/build \
&& meson install) && meson install)
touch harfbuzz-stamp touch harfbuzz-stamp
} }
function build_libavif {
if [ -e libavif-stamp ]; then return; fi
python3 -m pip install meson ninja
if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
fi
local build_type=MinSizeRel
local lto=ON
local libavif_cmake_flags
if [ -n "$IS_MACOS" ]; then
lto=OFF
libavif_cmake_flags=(
-DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \
)
else
if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then
build_type=Release
fi
libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now")
fi
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
# CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject
# of libavif) that disables support for encoding high bit depth images.
(cd $out_dir \
&& cmake \
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
-DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \
-DBUILD_SHARED_LIBS=ON \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_AOM=LOCAL \
-DCONFIG_AV1_HIGHBITDEPTH=0 \
-DAVIF_CODEC_AOM_DECODE=OFF \
-DAVIF_CODEC_DAV1D=LOCAL \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
-DCMAKE_C_VISIBILITY_PRESET=hidden \
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \
-DCMAKE_BUILD_TYPE=$build_type \
"${libavif_cmake_flags[@]}" \
. \
&& make install)
touch libavif-stamp
}
function build { function build {
build_xz build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
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
build_new_zlib
else
build_zlib_ng build_zlib_ng
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
@ -126,19 +186,18 @@ function build {
build_tiff build_tiff
fi fi
build_libavif
build_libpng build_libpng
build_lcms2 build_lcms2
build_openjpeg build_openjpeg
ORIGINAL_CFLAGS=$CFLAGS webp_cflags="-O3 -DNDEBUG"
CFLAGS="$CFLAGS -O3 -DNDEBUG"
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
fi fi
build_simple libwebp $LIBWEBP_VERSION \ CFLAGS="$CFLAGS $webp_cflags" 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
CFLAGS=$ORIGINAL_CFLAGS
build_brotli build_brotli

View File

@ -9,17 +9,21 @@ 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) {
$python = "pypy.exe"
} else {
$python = "python.exe"
}
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
if ("$venv" -like "*\cibw-run-*-win_amd64\*") { if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
& python -m pip install numpy & $venv\Scripts\$python -m pip install numpy
} }
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 Tests\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

@ -35,5 +35,5 @@ fi
# Runs tests # Runs tests
python3 selftest.py python3 selftest.py
python3 -m pytest Tests/check_wheel.py python3 -m pytest -vv -x Tests/check_wheel.py
python3 -m pytest python3 -m pytest -vv -x

View File

@ -58,12 +58,12 @@ jobs:
- name: "macOS 10.13 x86_64" - name: "macOS 10.13 x86_64"
os: macos-13 os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
build: "cp3{12,13}*" build: "cp3{12,13,14}*"
macosx_deployment_target: "10.13" macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64" - name: "macOS 10.15 x86_64"
os: macos-13 os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
build: "pp310*" build: "pp3*"
macosx_deployment_target: "10.15" macosx_deployment_target: "10.15"
- name: "macOS arm64" - name: "macOS arm64"
os: macos-latest os: macos-latest
@ -110,7 +110,6 @@ 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
@ -121,14 +120,17 @@ jobs:
windows: windows:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: Windows ${{ matrix.cibw_arch }} name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- cibw_arch: x86 - cibw_arch: x86
os: windows-latest
- cibw_arch: AMD64 - cibw_arch: AMD64
os: windows-latest
- cibw_arch: ARM64 - cibw_arch: ARM64
os: windows-11-arm
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -185,7 +187,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
@ -240,7 +241,7 @@ jobs:
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
uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2
with: with:
artifacts_path: dist artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}

7
.github/zizmor.yml vendored Normal file
View File

@ -0,0 +1,7 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/
rules:
unpinned-uses:
config:
policies:
"*": ref-pin

View File

@ -1,8 +1,8 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.4 rev: v0.12.0
hooks: hooks:
- id: ruff - id: ruff-check
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
@ -11,7 +11,7 @@ repos:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.8.2 rev: 1.8.5
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.7 rev: v20.1.6
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -44,20 +44,21 @@ repos:
- id: check-json - id: check-json
- id: check-toml - id: check-toml
- id: check-yaml - id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: ^Tests/images/ exclude: ^Tests/images/
- id: trailing-whitespace - id: trailing-whitespace
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.31.1 rev: 0.33.1
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
- id: check-renovate - id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit - repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.3.0 rev: v1.9.0
hooks: hooks:
- id: zizmor - id: zizmor
@ -67,12 +68,12 @@ 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.0 rev: v2.6.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.23 rev: v0.24.1
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject
additional_dependencies: [trove-classifiers>=2024.10.12] additional_dependencies: [trove-classifiers>=2024.10.12]

View File

@ -23,6 +23,10 @@ doc html:
htmlview: htmlview:
$(MAKE) -C docs htmlview $(MAKE) -C docs htmlview
.PHONY: htmllive
htmllive:
$(MAKE) -C docs htmllive
.PHONY: doccheck .PHONY: doccheck
doccheck: doccheck:
$(MAKE) doc $(MAKE) doc
@ -43,6 +47,7 @@ help:
@echo " docserve run an HTTP server on the docs directory" @echo " docserve run an HTTP server on the docs directory"
@echo " html make HTML docs" @echo " html make HTML docs"
@echo " htmlview open the index page built by the html target in your browser" @echo " htmlview open the index page built by the html target in your browser"
@echo " htmllive rebuild and reload HTML files in your browser"
@echo " install make and install" @echo " install make and install"
@echo " install-coverage make and install with C coverage" @echo " install-coverage make and install with C coverage"
@echo " lint run the lint checks" @echo " lint run the lint checks"
@ -92,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

@ -95,7 +95,7 @@ This library provides extensive file format support, an efficient internal repre
The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool.
## More Information ## More information
- [Documentation](https://pillow.readthedocs.io/) - [Documentation](https://pillow.readthedocs.io/)
- [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html)
@ -107,6 +107,6 @@ The core image library is designed for fast access to data stored in a few basic
- [Changelog](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/releases)
- [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
## Report a Vulnerability ## Report a vulnerability
To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security). To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security).

View File

@ -1,34 +1,15 @@
# Release Checklist # Release checklist
See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for
information about how the version numbers line up with releases. information about how the version numbers line up with releases.
## Main Release ## Main release
Released quarterly on January 2nd, April 1st, July 1st and October 15th. Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Create a new issue and select the "Maintainers only: Release" template.
* [ ] Develop and prepare release in `main` branch.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch. ## Point release
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
* [ ] Create branch and tag for release e.g.:
```bash
git branch 5.2.x
git tag 5.2.0
git push --tags
```
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases).
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
```bash
git push --all
```
## Point Release
Released as needed for security, installation or critical bug fixes. Released as needed for security, installation or critical bug fixes.
@ -58,7 +39,7 @@ Released as needed for security, installation or critical bug fixes.
git push git push
``` ```
## Embargoed Release ## Embargoed release
Released as needed privately to individual vendors for critical security-related bug fixes. Released as needed privately to individual vendors for critical security-related bug fixes.
@ -82,7 +63,7 @@ Released as needed privately to individual vendors for critical security-related
git push origin 2.5.x git push origin 2.5.x
``` ```
## Publicize Release ## Publicize release
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
@ -90,7 +71,7 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes * [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes
## Docker Images ## Docker images
* [ ] Update Pillow in the Docker Images repository * [ ] Update Pillow in the Docker Images repository
```bash ```bash

View File

@ -1,4 +1,4 @@
Pillow Tests Pillow tests
============ ============
Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``.

View File

@ -9,6 +9,6 @@ from PIL import Image
def test_j2k_overflow(tmp_path: Path) -> None: def test_j2k_overflow(tmp_path: Path) -> None:
im = Image.new("RGBA", (1024, 131584)) im = Image.new("RGBA", (1024, 131584))
target = str(tmp_path / "temp.jpc") target = tmp_path / "temp.jpc"
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(target) im.save(target)

View File

@ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
f = str(tmp_path / "temp.png") f = tmp_path / "temp.png"
im = Image.new("L", (xdim, ydim), 0) im = Image.new("L", (xdim, ydim), 0)
im.save(f) im.save(f)

View File

@ -28,7 +28,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
dtype = np.uint8 dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype) a = np.zeros((xdim, ydim), dtype=dtype)
f = str(tmp_path / "temp.png") f = tmp_path / "temp.png"
im = Image.fromarray(a, "L") im = Image.fromarray(a, "L")
im.save(f) im.save(f)

View File

@ -1,13 +1,17 @@
from __future__ import annotations from __future__ import annotations
import platform
import sys import sys
from PIL import features from PIL import features
from .helper import is_pypy
def test_wheel_modules() -> None: def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"}
if sys.platform == "win32":
# tkinter is not available in cibuildwheel installed CPython on Windows # tkinter is not available in cibuildwheel installed CPython on Windows
try: try:
import tkinter import tkinter
@ -16,6 +20,10 @@ def test_wheel_modules() -> None:
except ImportError: except ImportError:
expected_modules.remove("tkinter") expected_modules.remove("tkinter")
# libavif is not available on Windows for ARM64 architectures
if platform.machine() == "ARM64":
expected_modules.remove("avif")
assert set(features.get_supported_modules()) == expected_modules assert set(features.get_supported_modules()) == expected_modules
@ -40,5 +48,7 @@ def test_wheel_features() -> None:
if sys.platform == "win32": if sys.platform == "win32":
expected_features.remove("xcb") expected_features.remove("xcb")
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
expected_features.remove("zlib_ng")
assert set(features.get_supported_features()) == expected_features assert set(features.get_supported_features()) == expected_features

View File

@ -9,11 +9,11 @@ import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
import sysconfig
import tempfile import tempfile
from collections.abc import Sequence 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 from typing import Any, Callable
import pytest import pytest
@ -96,7 +96,10 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -
def assert_image_equal_tofile( def assert_image_equal_tofile(
a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None a: Image.Image,
filename: str | Path,
msg: str | None = None,
mode: str | None = None,
) -> None: ) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode: if mode:
@ -137,7 +140,7 @@ def assert_image_similar(
def assert_image_similar_tofile( def assert_image_similar_tofile(
a: Image.Image, a: Image.Image,
filename: str, filename: str | Path,
epsilon: float, epsilon: float,
msg: str | None = None, msg: str | None = None,
) -> None: ) -> None:
@ -158,6 +161,12 @@ def assert_tuple_approx_equal(
pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator:
if "PILLOW_VALGRIND_TEST" in os.environ:
return pytest.mark.pil_noop_mark()
return pytest.mark.timeout(timeout)
def skip_unless_feature(feature: str) -> pytest.MarkDecorator: def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
reason = f"{feature} not available" reason = f"{feature} not available"
return pytest.mark.skipif(not features.check(feature), reason=reason) return pytest.mark.skipif(not features.check(feature), reason=reason)
@ -263,7 +272,7 @@ def _cached_hopper(mode: str) -> Image.Image:
else: else:
im = hopper() im = hopper()
if mode.startswith("BGR;"): if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="BGR;"):
im = im.convert(mode) im = im.convert(mode)
else: else:
try: try:
@ -342,10 +351,6 @@ def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info") return hasattr(sys, "pypy_translation_info")
def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw"
class CachedProperty: class CachedProperty:
def __init__(self, func: Callable[[Any], Any]) -> None: def __init__(self, func: Callable[[Any], Any]) -> None:
self.func = func self.func = func

BIN
Tests/images/avif/exif.avif Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Tests/images/avif/star.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
Tests/images/avif/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

View File

@ -0,0 +1,260 @@
GIMP Palette
Name: fullpalette
Columns: 4
#
0 0 0 Index 0
1 1 1 Index 1
2 2 2 Index 2
3 3 3 Index 3
4 4 4 Index 4
5 5 5 Index 5
6 6 6 Index 6
7 7 7 Index 7
8 8 8 Index 8
9 9 9 Index 9
10 10 10 Index 10
11 11 11 Index 11
12 12 12 Index 12
13 13 13 Index 13
14 14 14 Index 14
15 15 15 Index 15
16 16 16 Index 16
17 17 17 Index 17
18 18 18 Index 18
19 19 19 Index 19
20 20 20 Index 20
21 21 21 Index 21
22 22 22 Index 22
23 23 23 Index 23
24 24 24 Index 24
25 25 25 Index 25
26 26 26 Index 26
27 27 27 Index 27
28 28 28 Index 28
29 29 29 Index 29
30 30 30 Index 30
31 31 31 Index 31
32 32 32 Index 32
33 33 33 Index 33
34 34 34 Index 34
35 35 35 Index 35
36 36 36 Index 36
37 37 37 Index 37
38 38 38 Index 38
39 39 39 Index 39
40 40 40 Index 40
41 41 41 Index 41
42 42 42 Index 42
43 43 43 Index 43
44 44 44 Index 44
45 45 45 Index 45
46 46 46 Index 46
47 47 47 Index 47
48 48 48 Index 48
49 49 49 Index 49
50 50 50 Index 50
51 51 51 Index 51
52 52 52 Index 52
53 53 53 Index 53
54 54 54 Index 54
55 55 55 Index 55
56 56 56 Index 56
57 57 57 Index 57
58 58 58 Index 58
59 59 59 Index 59
60 60 60 Index 60
61 61 61 Index 61
62 62 62 Index 62
63 63 63 Index 63
64 64 64 Index 64
65 65 65 Index 65
66 66 66 Index 66
67 67 67 Index 67
68 68 68 Index 68
69 69 69 Index 69
70 70 70 Index 70
71 71 71 Index 71
72 72 72 Index 72
73 73 73 Index 73
74 74 74 Index 74
75 75 75 Index 75
76 76 76 Index 76
77 77 77 Index 77
78 78 78 Index 78
79 79 79 Index 79
80 80 80 Index 80
81 81 81 Index 81
82 82 82 Index 82
83 83 83 Index 83
84 84 84 Index 84
85 85 85 Index 85
86 86 86 Index 86
87 87 87 Index 87
88 88 88 Index 88
89 89 89 Index 89
90 90 90 Index 90
91 91 91 Index 91
92 92 92 Index 92
93 93 93 Index 93
94 94 94 Index 94
95 95 95 Index 95
96 96 96 Index 96
97 97 97 Index 97
98 98 98 Index 98
99 99 99 Index 99
100 100 100 Index 100
101 101 101 Index 101
102 102 102 Index 102
103 103 103 Index 103
104 104 104 Index 104
105 105 105 Index 105
106 106 106 Index 106
107 107 107 Index 107
108 108 108 Index 108
109 109 109 Index 109
110 110 110 Index 110
111 111 111 Index 111
112 112 112 Index 112
113 113 113 Index 113
114 114 114 Index 114
115 115 115 Index 115
116 116 116 Index 116
117 117 117 Index 117
118 118 118 Index 118
119 119 119 Index 119
120 120 120 Index 120
121 121 121 Index 121
122 122 122 Index 122
123 123 123 Index 123
124 124 124 Index 124
125 125 125 Index 125
126 126 126 Index 126
127 127 127 Index 127
128 128 128 Index 128
129 129 129 Index 129
130 130 130 Index 130
131 131 131 Index 131
132 132 132 Index 132
133 133 133 Index 133
134 134 134 Index 134
135 135 135 Index 135
136 136 136 Index 136
137 137 137 Index 137
138 138 138 Index 138
139 139 139 Index 139
140 140 140 Index 140
141 141 141 Index 141
142 142 142 Index 142
143 143 143 Index 143
144 144 144 Index 144
145 145 145 Index 145
146 146 146 Index 146
147 147 147 Index 147
148 148 148 Index 148
149 149 149 Index 149
150 150 150 Index 150
151 151 151 Index 151
152 152 152 Index 152
153 153 153 Index 153
154 154 154 Index 154
155 155 155 Index 155
156 156 156 Index 156
157 157 157 Index 157
158 158 158 Index 158
159 159 159 Index 159
160 160 160 Index 160
161 161 161 Index 161
162 162 162 Index 162
163 163 163 Index 163
164 164 164 Index 164
165 165 165 Index 165
166 166 166 Index 166
167 167 167 Index 167
168 168 168 Index 168
169 169 169 Index 169
170 170 170 Index 170
171 171 171 Index 171
172 172 172 Index 172
173 173 173 Index 173
174 174 174 Index 174
175 175 175 Index 175
176 176 176 Index 176
177 177 177 Index 177
178 178 178 Index 178
179 179 179 Index 179
180 180 180 Index 180
181 181 181 Index 181
182 182 182 Index 182
183 183 183 Index 183
184 184 184 Index 184
185 185 185 Index 185
186 186 186 Index 186
187 187 187 Index 187
188 188 188 Index 188
189 189 189 Index 189
190 190 190 Index 190
191 191 191 Index 191
192 192 192 Index 192
193 193 193 Index 193
194 194 194 Index 194
195 195 195 Index 195
196 196 196 Index 196
197 197 197 Index 197
198 198 198 Index 198
199 199 199 Index 199
200 200 200 Index 200
201 201 201 Index 201
202 202 202 Index 202
203 203 203 Index 203
204 204 204 Index 204
205 205 205 Index 205
206 206 206 Index 206
207 207 207 Index 207
208 208 208 Index 208
209 209 209 Index 209
210 210 210 Index 210
211 211 211 Index 211
212 212 212 Index 212
213 213 213 Index 213
214 214 214 Index 214
215 215 215 Index 215
216 216 216 Index 216
217 217 217 Index 217
218 218 218 Index 218
219 219 219 Index 219
220 220 220 Index 220
221 221 221 Index 221
222 222 222 Index 222
223 223 223 Index 223
224 224 224 Index 224
225 225 225 Index 225
226 226 226 Index 226
227 227 227 Index 227
228 228 228 Index 228
229 229 229 Index 229
230 230 230 Index 230
231 231 231 Index 231
232 232 232 Index 232
233 233 233 Index 233
234 234 234 Index 234
235 235 235 Index 235
236 236 236 Index 236
237 237 237 Index 237
238 238 238 Index 238
239 239 239 Index 239
240 240 240 Index 240
241 241 241 Index 241
242 242 242 Index 242
243 243 243 Index 243
244 244 244 Index 244
245 245 245 Index 245
246 246 246 Index 246
247 247 247 Index 247
248 248 248 Index 248
249 249 249 Index 249
250 250 250 Index 250
251 251 251 Index 251
252 252 252 Index 252
253 253 253 Index 253
254 254 254 Index 254
255 255 255 Index 255

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.

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 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.

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

164
Tests/test_arrow.py Normal file
View File

@ -0,0 +1,164 @@
from __future__ import annotations
import pytest
from PIL import Image
from .helper import hopper
@pytest.mark.parametrize(
"mode, dest_modes",
(
("L", ["I", "F", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]),
("I", ["L", "F"]), # Technically I;32 can work for any 4x8bit storage.
("F", ["I", "L", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]),
("LA", ["L", "F"]),
("RGB", ["L", "F"]),
("RGBA", ["L", "F"]),
("RGBX", ["L", "F"]),
("CMYK", ["L", "F"]),
("YCbCr", ["L", "F"]),
("HSV", ["L", "F"]),
),
)
def test_invalid_array_type(mode: str, dest_modes: list[str]) -> None:
img = hopper(mode)
for dest_mode in dest_modes:
with pytest.raises(ValueError):
Image.fromarrow(img, dest_mode, img.size)
def test_invalid_array_size() -> None:
img = hopper("RGB")
assert img.size != (10, 10)
with pytest.raises(ValueError):
Image.fromarrow(img, "RGB", (10, 10))
def test_release_schema() -> None:
# these should not error out, valgrind should be clean
img = hopper("L")
schema = img.__arrow_c_schema__()
del schema
def test_release_array() -> None:
# these should not error out, valgrind should be clean
img = hopper("L")
array, schema = img.__arrow_c_array__()
del array
del schema
def test_readonly() -> None:
img = hopper("L")
reloaded = Image.fromarrow(img, img.mode, img.size)
assert reloaded.readonly == 1
reloaded._readonly = 0
assert reloaded.readonly == 1
def test_multiblock_l_image() -> None:
block_size = Image.core.get_block_size()
# check a 2 block image in single channel mode
size = (4096, 2 * block_size // 4096)
img = Image.new("L", size, 128)
with pytest.raises(ValueError):
(schema, arr) = img.__arrow_c_array__()
def test_multiblock_rgba_image() -> None:
block_size = Image.core.get_block_size()
# check a 2 block image in 4 channel mode
size = (4096, (block_size // 4096) // 2)
img = Image.new("RGBA", size, (128, 127, 126, 125))
with pytest.raises(ValueError):
(schema, arr) = img.__arrow_c_array__()
def test_multiblock_l_schema() -> None:
block_size = Image.core.get_block_size()
# check a 2 block image in single channel mode
size = (4096, 2 * block_size // 4096)
img = Image.new("L", size, 128)
with pytest.raises(ValueError):
img.__arrow_c_schema__()
def test_multiblock_rgba_schema() -> None:
block_size = Image.core.get_block_size()
# check a 2 block image in 4 channel mode
size = (4096, (block_size // 4096) // 2)
img = Image.new("RGBA", size, (128, 127, 126, 125))
with pytest.raises(ValueError):
img.__arrow_c_schema__()
def test_singleblock_l_image() -> None:
Image.core.set_use_block_allocator(1)
block_size = Image.core.get_block_size()
# check a 2 block image in 4 channel mode
size = (4096, 2 * (block_size // 4096))
img = Image.new("L", size, 128)
assert img.im.isblock()
(schema, arr) = img.__arrow_c_array__()
assert schema
assert arr
Image.core.set_use_block_allocator(0)
def test_singleblock_rgba_image() -> None:
Image.core.set_use_block_allocator(1)
block_size = Image.core.get_block_size()
# check a 2 block image in 4 channel mode
size = (4096, (block_size // 4096) // 2)
img = Image.new("RGBA", size, (128, 127, 126, 125))
assert img.im.isblock()
(schema, arr) = img.__arrow_c_array__()
assert schema
assert arr
Image.core.set_use_block_allocator(0)
def test_singleblock_l_schema() -> None:
Image.core.set_use_block_allocator(1)
block_size = Image.core.get_block_size()
# check a 2 block image in single channel mode
size = (4096, 2 * block_size // 4096)
img = Image.new("L", size, 128)
assert img.im.isblock()
schema = img.__arrow_c_schema__()
assert schema
Image.core.set_use_block_allocator(0)
def test_singleblock_rgba_schema() -> None:
Image.core.set_use_block_allocator(1)
block_size = Image.core.get_block_size()
# check a 2 block image in 4 channel mode
size = (4096, (block_size // 4096) // 2)
img = Image.new("RGBA", size, (128, 127, 126, 125))
assert img.im.isblock()
schema = img.__arrow_c_schema__()
assert schema
Image.core.set_use_block_allocator(0)

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

@ -47,7 +47,6 @@ 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)

View File

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

View File

@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin
# (referenced from https://wiki.mozilla.org/APNG_Specification) # (referenced from https://wiki.mozilla.org/APNG_Specification)
def test_apng_basic() -> None: def test_apng_basic() -> None:
with Image.open("Tests/images/apng/single_frame.png") as im: with Image.open("Tests/images/apng/single_frame.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated assert not im.is_animated
assert im.n_frames == 1 assert im.n_frames == 1
assert im.get_format_mimetype() == "image/apng" assert im.get_format_mimetype() == "image/apng"
@ -20,6 +21,7 @@ def test_apng_basic() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/single_frame_default.png") as im: with Image.open("Tests/images/apng/single_frame_default.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.is_animated assert im.is_animated
assert im.n_frames == 2 assert im.n_frames == 2
assert im.get_format_mimetype() == "image/apng" assert im.get_format_mimetype() == "image/apng"
@ -34,8 +36,11 @@ def test_apng_basic() -> None:
with pytest.raises(EOFError): with pytest.raises(EOFError):
im.seek(2) im.seek(2)
# test rewind support
im.seek(0) im.seek(0)
with pytest.raises(ValueError, match="cannot seek to frame 2"):
im._seek(2)
# test rewind support
assert im.getpixel((0, 0)) == (255, 0, 0, 255) assert im.getpixel((0, 0)) == (255, 0, 0, 255)
assert im.getpixel((64, 32)) == (255, 0, 0, 255) assert im.getpixel((64, 32)) == (255, 0, 0, 255)
im.seek(1) im.seek(1)
@ -49,6 +54,7 @@ def test_apng_basic() -> None:
) )
def test_apng_fdat(filename: str) -> None: def test_apng_fdat(filename: str) -> None:
with Image.open(filename) as im: with Image.open(filename) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -56,31 +62,37 @@ def test_apng_fdat(filename: str) -> None:
def test_apng_dispose() -> None: def test_apng_dispose() -> None:
with Image.open("Tests/images/apng/dispose_op_none.png") as im: with Image.open("Tests/images/apng/dispose_op_none.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_background.png") as im: with Image.open("Tests/images/apng/dispose_op_background.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: with Image.open("Tests/images/apng/dispose_op_background_final.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_previous.png") as im: with Image.open("Tests/images/apng/dispose_op_previous.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
@ -88,21 +100,25 @@ def test_apng_dispose() -> None:
def test_apng_dispose_region() -> None: def test_apng_dispose_region() -> None:
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: with Image.open("Tests/images/apng/dispose_op_background_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 255, 255) assert im.getpixel((0, 0)) == (0, 0, 255, 255)
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -129,6 +145,7 @@ def test_apng_dispose_op_previous_frame() -> None:
# ], # ],
# ) # )
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (255, 0, 0, 255) assert im.getpixel((0, 0)) == (255, 0, 0, 255)
@ -142,26 +159,31 @@ def test_apng_dispose_op_background_p_mode() -> None:
def test_apng_blend() -> None: def test_apng_blend() -> None:
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 2) assert im.getpixel((0, 0)) == (0, 255, 0, 2)
assert im.getpixel((64, 32)) == (0, 255, 0, 2) assert im.getpixel((64, 32)) == (0, 255, 0, 2)
with Image.open("Tests/images/apng/blend_op_over.png") as im: with Image.open("Tests/images/apng/blend_op_over.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 97) assert im.getpixel((0, 0)) == (0, 255, 0, 97)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -175,6 +197,7 @@ def test_apng_blend_transparency() -> None:
def test_apng_chunk_order() -> None: def test_apng_chunk_order() -> None:
with Image.open("Tests/images/apng/fctl_actl.png") as im: with Image.open("Tests/images/apng/fctl_actl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -230,24 +253,28 @@ def test_apng_num_plays() -> None:
def test_apng_mode() -> None: def test_apng_mode() -> None:
with Image.open("Tests/images/apng/mode_16bit.png") as im: with Image.open("Tests/images/apng/mode_16bit.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "RGBA" assert im.mode == "RGBA"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 128, 191) assert im.getpixel((0, 0)) == (0, 0, 128, 191)
assert im.getpixel((64, 32)) == (0, 0, 128, 191) assert im.getpixel((64, 32)) == (0, 0, 128, 191)
with Image.open("Tests/images/apng/mode_grayscale.png") as im: with Image.open("Tests/images/apng/mode_grayscale.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "L" assert im.mode == "L"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == 128 assert im.getpixel((0, 0)) == 128
assert im.getpixel((64, 32)) == 255 assert im.getpixel((64, 32)) == 255
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "LA" assert im.mode == "LA"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (128, 191) assert im.getpixel((0, 0)) == (128, 191)
assert im.getpixel((64, 32)) == (128, 191) assert im.getpixel((64, 32)) == (128, 191)
with Image.open("Tests/images/apng/mode_palette.png") as im: with Image.open("Tests/images/apng/mode_palette.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im = im.convert("RGB") im = im.convert("RGB")
@ -255,6 +282,7 @@ def test_apng_mode() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0) assert im.getpixel((64, 32)) == (0, 255, 0)
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im = im.convert("RGBA") im = im.convert("RGBA")
@ -262,6 +290,7 @@ def test_apng_mode() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im = im.convert("RGBA") im = im.convert("RGBA")
@ -271,54 +300,68 @@ def test_apng_mode() -> None:
def test_apng_chunk_errors() -> None: def test_apng_chunk_errors() -> None:
with Image.open("Tests/images/apng/chunk_no_actl.png") as im: with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
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 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 not im.is_animated assert not im.is_animated
with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: with Image.open("Tests/images/apng/chunk_no_fctl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: with Image.open("Tests/images/apng/chunk_no_fdat.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
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 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 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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
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 not im.is_animated assert not im.is_animated
im.load() im.load()
im.close()
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -336,16 +379,18 @@ def test_apng_syntax_errors() -> None:
def test_apng_sequence_errors(test_file: str) -> None: def test_apng_sequence_errors(test_file: str) -> None:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
with Image.open(f"Tests/images/apng/{test_file}") as im: with Image.open(f"Tests/images/apng/{test_file}") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() im.load()
def test_apng_save(tmp_path: Path) -> None: def test_apng_save(tmp_path: Path) -> None:
with Image.open("Tests/images/apng/single_frame.png") as im: with Image.open("Tests/images/apng/single_frame.png") as im:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file, save_all=True) im.save(test_file, save_all=True)
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.load() im.load()
assert not im.is_animated assert not im.is_animated
assert im.n_frames == 1 assert im.n_frames == 1
@ -361,6 +406,7 @@ def test_apng_save(tmp_path: Path) -> None:
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.load() im.load()
assert im.is_animated assert im.is_animated
assert im.n_frames == 2 assert im.n_frames == 2
@ -372,7 +418,7 @@ def test_apng_save(tmp_path: Path) -> None:
def test_apng_save_alpha(tmp_path: Path) -> None: def test_apng_save_alpha(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127))
@ -390,7 +436,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
# frames with image data spanning multiple fdAT chunks (in this case # frames with image data spanning multiple fdAT chunks (in this case
# both the default image and first animation frame will span multiple # both the default image and first animation frame will span multiple
# data chunks) # data chunks)
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
with Image.open("Tests/images/old-style-jpeg-compression.png") as im: with Image.open("Tests/images/old-style-jpeg-compression.png") as im:
frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))]
im.save( im.save(
@ -400,12 +446,13 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
append_images=frames, append_images=frames,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() im.load()
def test_apng_save_duration_loop(tmp_path: Path) -> None: def test_apng_save_duration_loop(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
with Image.open("Tests/images/apng/delay.png") as im: with Image.open("Tests/images/apng/delay.png") as im:
frames = [] frames = []
durations = [] durations = []
@ -442,6 +489,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert "duration" not in im.info assert "duration" not in im.info
@ -453,6 +501,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
duration=[500, 100, 150], duration=[500, 100, 150],
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 2 assert im.n_frames == 2
assert im.info["duration"] == 600 assert im.info["duration"] == 600
@ -463,12 +512,13 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
frame.info["duration"] = 300 frame.info["duration"] = 300
frame.save(test_file, save_all=True, append_images=[frame, different_frame]) frame.save(test_file, save_all=True, append_images=[frame, different_frame])
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 2 assert im.n_frames == 2
assert im.info["duration"] == 600 assert im.info["duration"] == 600
def test_apng_save_disposal(tmp_path: Path) -> None: def test_apng_save_disposal(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
size = (128, 64) size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
@ -569,7 +619,7 @@ def test_apng_save_disposal(tmp_path: Path) -> None:
def test_apng_save_disposal_previous(tmp_path: Path) -> None: def test_apng_save_disposal_previous(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
size = (128, 64) size = (128, 64)
blue = Image.new("RGBA", size, (0, 0, 255, 255)) blue = Image.new("RGBA", size, (0, 0, 255, 255))
red = Image.new("RGBA", size, (255, 0, 0, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255))
@ -591,7 +641,7 @@ def test_apng_save_disposal_previous(tmp_path: Path) -> None:
def test_apng_save_blend(tmp_path: Path) -> None: def test_apng_save_blend(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
size = (128, 64) size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
@ -659,7 +709,7 @@ def test_apng_save_blend(tmp_path: Path) -> None:
def test_apng_save_size(tmp_path: Path) -> None: def test_apng_save_size(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im = Image.new("L", (100, 100)) im = Image.new("L", (100, 100))
im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))]) im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
@ -683,7 +733,7 @@ def test_seek_after_close() -> None:
def test_different_modes_in_later_frames( def test_different_modes_in_later_frames(
mode: str, default_image: bool, duplicate: bool, tmp_path: Path mode: str, default_image: bool, duplicate: bool, tmp_path: Path
) -> None: ) -> None:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
im.save( im.save(
@ -697,7 +747,7 @@ def test_different_modes_in_later_frames(
def test_different_durations(tmp_path: Path) -> None: def test_different_durations(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
with Image.open("Tests/images/apng/different_durations.png") as im: with Image.open("Tests/images/apng/different_durations.png") as im:
for _ in range(3): for _ in range(3):

781
Tests/test_file_avif.py Normal file
View File

@ -0,0 +1,781 @@
from __future__ import annotations
import gc
import os
import re
import warnings
from collections.abc import Generator, Sequence
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
from typing import Any
import pytest
from PIL import (
AvifImagePlugin,
Image,
ImageDraw,
ImageFile,
UnidentifiedImageError,
features,
)
from .helper import (
PillowLeakTestCase,
assert_image,
assert_image_similar,
assert_image_similar_tofile,
hopper,
skip_unless_feature,
)
try:
from PIL import _avif
HAVE_AVIF = True
except ImportError:
HAVE_AVIF = False
TEST_AVIF_FILE = "Tests/images/avif/hopper.avif"
def assert_xmp_orientation(xmp: bytes, expected: int) -> None:
assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected
def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile:
out = BytesIO()
im.save(out, "AVIF", **options)
return Image.open(out)
def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator:
reason = f"{codec_name} decode not available"
return pytest.mark.skipif(
not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason
)
def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator:
reason = f"{codec_name} encode not available"
return pytest.mark.skipif(
not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason
)
def is_docker_qemu() -> bool:
try:
init_proc_exe = os.readlink("/proc/1/exe")
except (FileNotFoundError, PermissionError):
return False
return "qemu" in init_proc_exe
class TestUnsupportedAvif:
def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
with pytest.raises(UnidentifiedImageError):
with pytest.warns(UserWarning, match="AVIF support not installed"):
with Image.open(TEST_AVIF_FILE):
pass
def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
with pytest.raises(SyntaxError):
AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE)
@skip_unless_feature("avif")
class TestFileAvif:
def test_version(self) -> None:
version = features.version_module("avif")
assert version is not None
assert re.search(r"^\d+\.\d+\.\d+$", version)
def test_codec_version(self) -> None:
assert AvifImagePlugin.get_codec_version("unknown") is None
for codec_name in ("aom", "dav1d", "rav1e", "svt"):
codec_version = AvifImagePlugin.get_codec_version(codec_name)
if _avif.decoder_codec_available(
codec_name
) or _avif.encoder_codec_available(codec_name):
assert codec_version is not None
assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version)
else:
assert codec_version is None
def test_read(self) -> None:
"""
Can we read an AVIF file without error?
Does it have the bits we expect?
"""
with Image.open(TEST_AVIF_FILE) as image:
assert image.mode == "RGB"
assert image.size == (128, 128)
assert image.format == "AVIF"
assert image.get_format_mimetype() == "image/avif"
image.getdata()
# generated with:
# avifdec hopper.avif hopper_avif_write.png
assert_image_similar_tofile(
image, "Tests/images/avif/hopper_avif_write.png", 11.5
)
def test_write_rgb(self, tmp_path: Path) -> None:
"""
Can we write a RGB mode file to avif without error?
Does it have the bits we expect?
"""
temp_file = tmp_path / "temp.avif"
im = hopper()
im.save(temp_file)
with Image.open(temp_file) as reloaded:
assert reloaded.mode == "RGB"
assert reloaded.size == (128, 128)
assert reloaded.format == "AVIF"
reloaded.getdata()
# avifdec hopper.avif avif/hopper_avif_write.png
assert_image_similar_tofile(
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02
)
# This test asserts that the images are similar. If the average pixel
# difference between the two images is less than the epsilon value,
# then we're going to accept that it's a reasonable lossy version of
# the image.
assert_image_similar(reloaded, im, 8.62)
def test_AvifEncoder_with_invalid_args(self) -> None:
"""
Calling encoder functions with no arguments should result in an error.
"""
with pytest.raises(TypeError):
_avif.AvifEncoder()
def test_AvifDecoder_with_invalid_args(self) -> None:
"""
Calling decoder functions with no arguments should result in an error.
"""
with pytest.raises(TypeError):
_avif.AvifDecoder()
def test_invalid_dimensions(self, tmp_path: Path) -> None:
test_file = tmp_path / "temp.avif"
im = Image.new("RGB", (0, 0))
with pytest.raises(ValueError):
im.save(test_file)
def test_encoder_finish_none_error(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Save should raise an OSError if AvifEncoder.finish returns None"""
class _mock_avif:
class AvifEncoder:
def __init__(self, *args: Any) -> None:
pass
def add(self, *args: Any) -> None:
pass
def finish(self) -> None:
return None
monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif)
im = Image.new("RGB", (150, 150))
test_file = tmp_path / "temp.avif"
with pytest.raises(OSError):
im.save(test_file)
def test_no_resource_warning(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
with warnings.catch_warnings():
warnings.simplefilter("error")
im.save(tmp_path / "temp.avif")
@pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"])
def test_accept_ftyp_brands(self, major_brand: bytes) -> None:
data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand
assert AvifImagePlugin._accept(data) is True
def test_file_pointer_could_be_reused(self) -> None:
with open(TEST_AVIF_FILE, "rb") as blob:
with Image.open(blob) as im:
im.load()
with Image.open(blob) as im:
im.load()
def test_background_from_gif(self, tmp_path: Path) -> None:
with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1))
# Save as AVIF
out_avif = tmp_path / "temp.avif"
im.save(out_avif, save_all=True)
# Save as GIF
out_gif = tmp_path / "temp.gif"
with Image.open(out_avif) as im:
im.save(out_gif)
with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1))
difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)])
assert difference <= 6
def test_save_single_frame(self, tmp_path: Path) -> None:
temp_file = tmp_path / "temp.avif"
with Image.open("Tests/images/chi.gif") as im:
im.save(temp_file)
with Image.open(temp_file) as im:
assert im.n_frames == 1
def test_invalid_file(self) -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
AvifImagePlugin.AvifImageFile(invalid_file)
def test_load_transparent_rgb(self) -> None:
test_file = "Tests/images/avif/transparency.avif"
with Image.open(test_file) as im:
assert_image(im, "RGBA", (64, 64))
# image has 876 transparent pixels
colors = im.getchannel("A").getcolors()
assert colors is not None
assert colors[0] == (876, 0)
def test_save_transparent(self, tmp_path: Path) -> None:
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
assert im.getcolors() == [(100, (0, 0, 0, 0))]
test_file = tmp_path / "temp.avif"
im.save(test_file)
# check if saved image contains the same transparency
with Image.open(test_file) as im:
assert_image(im, "RGBA", (10, 10))
assert im.getcolors() == [(100, (0, 0, 0, 0))]
def test_save_icc_profile(self) -> None:
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
assert "icc_profile" not in im.info
with Image.open("Tests/images/avif/icc_profile.avif") as with_icc:
expected_icc = with_icc.info["icc_profile"]
assert expected_icc is not None
im = roundtrip(im, icc_profile=expected_icc)
assert im.info["icc_profile"] == expected_icc
def test_discard_icc_profile(self) -> None:
with Image.open("Tests/images/avif/icc_profile.avif") as im:
im = roundtrip(im, icc_profile=None)
assert "icc_profile" not in im.info
def test_roundtrip_icc_profile(self) -> None:
with Image.open("Tests/images/avif/icc_profile.avif") as im:
expected_icc = im.info["icc_profile"]
im = roundtrip(im)
assert im.info["icc_profile"] == expected_icc
def test_roundtrip_no_icc_profile(self) -> None:
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
assert "icc_profile" not in im.info
im = roundtrip(im)
assert "icc_profile" not in im.info
def test_exif(self) -> None:
# With an EXIF chunk
with Image.open("Tests/images/avif/exif.avif") as im:
exif = im.getexif()
assert exif[274] == 1
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
exif = im.getexif()
assert exif[274] == 3
@pytest.mark.parametrize("use_bytes", [True, False])
@pytest.mark.parametrize("orientation", [1, 2, 3, 4, 5, 6, 7, 8])
def test_exif_save(
self,
tmp_path: Path,
use_bytes: bool,
orientation: int,
) -> None:
exif = Image.Exif()
exif[274] = orientation
exif_data = exif.tobytes()
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, exif=exif_data if use_bytes else exif)
with Image.open(test_file) as reloaded:
if orientation == 1:
assert "exif" not in reloaded.info
else:
assert reloaded.getexif()[274] == orientation
assert reloaded.info["exif"] == exif_data
def test_exif_without_orientation(self, tmp_path: Path) -> None:
exif = Image.Exif()
exif[272] = b"test"
exif_data = exif.tobytes()
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, exif=exif)
with Image.open(test_file) as reloaded:
assert reloaded.info["exif"] == exif_data
def test_exif_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(SyntaxError):
im.save(test_file, exif=b"invalid")
@pytest.mark.parametrize(
"rot, mir, exif_orientation",
[
(0, 0, 4),
(0, 1, 2),
(1, 0, 5),
(1, 1, 7),
(2, 0, 2),
(2, 1, 4),
(3, 0, 7),
(3, 1, 5),
],
)
def test_rot_mir_exif(
self, rot: int, mir: int, exif_orientation: int, tmp_path: Path
) -> None:
with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im:
exif = im.getexif()
assert exif[274] == exif_orientation
test_file = tmp_path / "temp.avif"
im.save(test_file, exif=exif)
with Image.open(test_file) as reloaded:
assert reloaded.getexif()[274] == exif_orientation
def test_xmp(self) -> None:
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
xmp = im.info["xmp"]
assert_xmp_orientation(xmp, 3)
def test_xmp_save(self, tmp_path: Path) -> None:
xmp_arg = "\n".join(
[
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>',
'<x:xmpmeta xmlns:x="adobe:ns:meta/">',
' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">',
' <rdf:Description rdf:about=""',
' xmlns:tiff="http://ns.adobe.com/tiff/1.0/"',
' tiff:Orientation="1"/>',
" </rdf:RDF>",
"</x:xmpmeta>",
'<?xpacket end="r"?>',
]
)
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, xmp=xmp_arg)
with Image.open(test_file) as reloaded:
xmp = reloaded.info["xmp"]
assert_xmp_orientation(xmp, 1)
def test_tell(self) -> None:
with Image.open(TEST_AVIF_FILE) as im:
assert im.tell() == 0
def test_seek(self) -> None:
with Image.open(TEST_AVIF_FILE) as im:
im.seek(0)
with pytest.raises(EOFError):
im.seek(1)
@pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:2:0", "4:0:0"])
def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, subsampling=subsampling)
def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, subsampling="foo")
@pytest.mark.parametrize("value", ["full", "limited"])
def test_encoder_range(self, tmp_path: Path, value: str) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, range=value)
def test_encoder_range_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, range="foo")
@skip_unless_avif_encoder("aom")
def test_encoder_codec_param(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
im.save(test_file, codec="aom")
def test_encoder_codec_invalid(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, codec="foo")
@skip_unless_avif_decoder("dav1d")
def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, codec="dav1d")
@skip_unless_avif_encoder("aom")
@pytest.mark.parametrize(
"advanced",
[
{
"aq-mode": "1",
"enable-chroma-deltaq": "1",
},
(("aq-mode", "1"), ("enable-chroma-deltaq", "1")),
[("aq-mode", "1"), ("enable-chroma-deltaq", "1")],
],
)
def test_encoder_advanced_codec_options(
self, advanced: dict[str, str] | Sequence[tuple[str, str]]
) -> None:
with Image.open(TEST_AVIF_FILE) as im:
ctrl_buf = BytesIO()
im.save(ctrl_buf, "AVIF", codec="aom")
test_buf = BytesIO()
im.save(
test_buf,
"AVIF",
codec="aom",
advanced=advanced,
)
assert ctrl_buf.getvalue() != test_buf.getvalue()
@skip_unless_avif_encoder("aom")
@pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234])
def test_encoder_advanced_codec_options_invalid(
self, tmp_path: Path, advanced: dict[str, str] | int
) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, codec="aom", advanced=advanced)
@skip_unless_avif_decoder("aom")
def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom")
with Image.open(TEST_AVIF_FILE) as im:
assert im.size == (128, 128)
@skip_unless_avif_encoder("rav1e")
def test_encoder_codec_cannot_decode(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e")
with pytest.raises(ValueError):
with Image.open(TEST_AVIF_FILE):
pass
def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo")
with pytest.raises(ValueError):
with Image.open(TEST_AVIF_FILE):
pass
@skip_unless_avif_encoder("aom")
def test_encoder_codec_available(self) -> None:
assert _avif.encoder_codec_available("aom") is True
def test_encoder_codec_available_bad_params(self) -> None:
with pytest.raises(TypeError):
_avif.encoder_codec_available()
@skip_unless_avif_decoder("dav1d")
def test_encoder_codec_available_cannot_decode(self) -> None:
assert _avif.encoder_codec_available("dav1d") is False
def test_encoder_codec_available_invalid(self) -> None:
assert _avif.encoder_codec_available("foo") is False
def test_encoder_quality_valueerror(self, tmp_path: Path) -> None:
with Image.open(TEST_AVIF_FILE) as im:
test_file = tmp_path / "temp.avif"
with pytest.raises(ValueError):
im.save(test_file, quality="invalid")
@skip_unless_avif_decoder("aom")
def test_decoder_codec_available(self) -> None:
assert _avif.decoder_codec_available("aom") is True
def test_decoder_codec_available_bad_params(self) -> None:
with pytest.raises(TypeError):
_avif.decoder_codec_available()
@skip_unless_avif_encoder("rav1e")
def test_decoder_codec_available_cannot_decode(self) -> None:
assert _avif.decoder_codec_available("rav1e") is False
def test_decoder_codec_available_invalid(self) -> None:
assert _avif.decoder_codec_available("foo") is False
def test_p_mode_transparency(self, tmp_path: Path) -> None:
im = Image.new("P", size=(64, 64))
draw = ImageDraw.Draw(im)
draw.rectangle(xy=[(0, 0), (32, 32)], fill=255)
draw.rectangle(xy=[(32, 32), (64, 64)], fill=255)
out_png = tmp_path / "temp.png"
im.save(out_png, transparency=0)
with Image.open(out_png) as im_png:
out_avif = tmp_path / "temp.avif"
im_png.save(out_avif, quality=100)
with Image.open(out_avif) as expected:
assert_image_similar(im_png.convert("RGBA"), expected, 0.17)
def test_decoder_strict_flags(self) -> None:
# This would fail if full avif strictFlags were enabled
with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im:
assert im.size == (128, 128)
@skip_unless_avif_encoder("aom")
@pytest.mark.parametrize("speed", [-1, 1, 11])
def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None:
test_file = tmp_path / "temp.avif"
hopper().save(test_file, codec="aom", speed=speed)
@skip_unless_avif_encoder("svt")
def test_svt_optimizations(self, tmp_path: Path) -> None:
test_file = tmp_path / "temp.avif"
hopper().save(test_file, codec="svt", speed=1)
@skip_unless_feature("avif")
class TestAvifAnimation:
@contextmanager
def star_frames(self) -> Generator[list[Image.Image], None, None]:
with Image.open("Tests/images/avif/star.png") as f:
yield [f, f.rotate(90), f.rotate(180), f.rotate(270)]
def test_n_frames(self) -> None:
"""
Ensure that AVIF format sets n_frames and is_animated attributes
correctly.
"""
with Image.open(TEST_AVIF_FILE) as im:
assert im.n_frames == 1
assert not im.is_animated
with Image.open("Tests/images/avif/star.avifs") as im:
assert im.n_frames == 5
assert im.is_animated
def test_write_animation_P(self, tmp_path: Path) -> None:
"""
Convert an animated GIF to animated AVIF, then compare the frame
count, and ensure the frames are visually similar to the originals.
"""
with Image.open("Tests/images/avif/star.gif") as original:
assert original.n_frames > 1
temp_file = tmp_path / "temp.avif"
original.save(temp_file, save_all=True)
with Image.open(temp_file) as im:
assert im.n_frames == original.n_frames
# Compare first frame in P mode to frame from original GIF
assert_image_similar(im, original.convert("RGBA"), 2)
# Compare later frames in RGBA mode to frames from original GIF
for frame in range(1, original.n_frames):
original.seek(frame)
im.seek(frame)
assert_image_similar(im, original, 2.54)
def test_write_animation_RGBA(self, tmp_path: Path) -> None:
"""
Write an animated AVIF from RGBA frames, and ensure the frames
are visually similar to the originals.
"""
def check(temp_file: Path) -> None:
with Image.open(temp_file) as im:
assert im.n_frames == 4
# Compare first frame to original
assert_image_similar(im, frame1, 2.7)
# Compare second frame to original
im.seek(1)
assert_image_similar(im, frame2, 4.1)
with self.star_frames() as frames:
frame1 = frames[0]
frame2 = frames[1]
temp_file1 = tmp_path / "temp.avif"
frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:])
check(temp_file1)
# Test appending using a generator
def imGenerator(
ims: list[Image.Image],
) -> Generator[Image.Image, None, None]:
yield from ims
temp_file2 = tmp_path / "temp_generator.avif"
frames[0].copy().save(
temp_file2,
save_all=True,
append_images=imGenerator(frames[1:]),
)
check(temp_file2)
def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None:
temp_file = tmp_path / "temp.avif"
frame1 = Image.new("RGB", (100, 100))
frame2 = Image.new("RGB", (150, 150))
with pytest.raises(ValueError):
frame1.save(temp_file, save_all=True, append_images=[frame2])
def test_heif_raises_unidentified_image_error(self) -> None:
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/avif/hopper.heif"):
pass
@pytest.mark.parametrize("alpha_premultiplied", [False, True])
def test_alpha_premultiplied(
self, tmp_path: Path, alpha_premultiplied: bool
) -> None:
temp_file = tmp_path / "temp.avif"
color = (200, 200, 200, 1)
im = Image.new("RGBA", (1, 1), color)
im.save(temp_file, alpha_premultiplied=alpha_premultiplied)
expected = (255, 255, 255, 1) if alpha_premultiplied else color
with Image.open(temp_file) as reloaded:
assert reloaded.getpixel((0, 0)) == expected
def test_timestamp_and_duration(self, tmp_path: Path) -> None:
"""
Try passing a list of durations, and make sure the encoded
timestamps and durations are correct.
"""
durations = [1, 10, 20, 30, 40]
temp_file = tmp_path / "temp.avif"
with self.star_frames() as frames:
frames[0].save(
temp_file,
save_all=True,
append_images=(frames[1:] + [frames[0]]),
duration=durations,
)
with Image.open(temp_file) as im:
assert im.n_frames == 5
assert im.is_animated
# Check that timestamps and durations match original values specified
timestamp = 0
for frame in range(im.n_frames):
im.seek(frame)
im.load()
assert im.info["duration"] == durations[frame]
assert im.info["timestamp"] == timestamp
timestamp += durations[frame]
def test_seeking(self, tmp_path: Path) -> None:
"""
Create an animated AVIF file, and then try seeking through frames in
reverse-order, verifying the timestamps and durations are correct.
"""
duration = 33
temp_file = tmp_path / "temp.avif"
with self.star_frames() as frames:
frames[0].save(
temp_file,
save_all=True,
append_images=(frames[1:] + [frames[0]]),
duration=duration,
)
with Image.open(temp_file) as im:
assert im.n_frames == 5
assert im.is_animated
# Traverse frames in reverse, checking timestamps and durations
timestamp = duration * (im.n_frames - 1)
for frame in reversed(range(im.n_frames)):
im.seek(frame)
im.load()
assert im.info["duration"] == duration
assert im.info["timestamp"] == timestamp
timestamp -= duration
def test_seek_errors(self) -> None:
with Image.open("Tests/images/avif/star.avifs") as im:
with pytest.raises(EOFError):
im.seek(-1)
with pytest.raises(EOFError):
im.seek(42)
MAX_THREADS = os.cpu_count() or 1
@skip_unless_feature("avif")
class TestAvifLeaks(PillowLeakTestCase):
mem_limit = MAX_THREADS * 3 * 1024
iterations = 100
@pytest.mark.skipif(
is_docker_qemu(), reason="Skipping on cross-architecture containers"
)
def test_leak_load(self) -> None:
with open(TEST_AVIF_FILE, "rb") as f:
im_data = f.read()
def core() -> None:
with Image.open(BytesIO(im_data)) as im:
im.load()
gc.collect()
self._test_leak(core)

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,
) )
@ -46,24 +45,22 @@ def test_invalid_file() -> None:
def test_save(tmp_path: Path) -> None: def test_save(tmp_path: Path) -> None:
f = str(tmp_path / "temp.blp") f = tmp_path / "temp.blp"
for version in ("BLP1", "BLP2"): for version in ("BLP1", "BLP2"):
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 = str(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

@ -15,10 +15,11 @@ from .helper import (
) )
def test_sanity(tmp_path: Path) -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
def roundtrip(im: Image.Image) -> None: def test_sanity(mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.bmp") outfile = tmp_path / "temp.bmp"
im = hopper(mode)
im.save(outfile, "BMP") im.save(outfile, "BMP")
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
@ -28,13 +29,6 @@ def test_sanity(tmp_path: Path) -> None:
assert reloaded.format == "BMP" assert reloaded.format == "BMP"
assert reloaded.get_format_mimetype() == "image/bmp" assert reloaded.get_format_mimetype() == "image/bmp"
roundtrip(hopper())
roundtrip(hopper("1"))
roundtrip(hopper("L"))
roundtrip(hopper("P"))
roundtrip(hopper("RGB"))
def test_invalid_file() -> None: def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp: with open("Tests/images/flower.jpg", "rb") as fp:
@ -66,7 +60,7 @@ def test_small_palette(tmp_path: Path) -> None:
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
im.putpalette(colors) im.putpalette(colors)
out = str(tmp_path / "temp.bmp") out = tmp_path / "temp.bmp"
im.save(out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -74,7 +68,7 @@ def test_small_palette(tmp_path: Path) -> None:
def test_save_too_large(tmp_path: Path) -> None: def test_save_too_large(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.bmp") outfile = tmp_path / "temp.bmp"
with Image.new("RGB", (1, 1)) as im: with Image.new("RGB", (1, 1)) as im:
im._size = (37838, 37838) im._size = (37838, 37838)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -96,7 +90,7 @@ def test_dpi() -> None:
def test_save_bmp_with_dpi(tmp_path: Path) -> None: def test_save_bmp_with_dpi(tmp_path: Path) -> None:
# Test for #1301 # Test for #1301
# Arrange # Arrange
outfile = str(tmp_path / "temp.jpg") outfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.bmp") as im: with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) assert im.info["dpi"] == (95.98654816726399, 95.98654816726399)
@ -112,7 +106,7 @@ def test_save_bmp_with_dpi(tmp_path: Path) -> None:
def test_save_float_dpi(tmp_path: Path) -> None: def test_save_float_dpi(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.bmp") outfile = tmp_path / "temp.bmp"
with Image.open("Tests/images/hopper.bmp") as im: with Image.open("Tests/images/hopper.bmp") as im:
im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
@ -152,7 +146,7 @@ def test_dib_header_size(header_size: int, path: str) -> None:
def test_save_dib(tmp_path: Path) -> None: def test_save_dib(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.dib") outfile = tmp_path / "temp.dib"
with Image.open("Tests/images/clipboard.dib") as im: with Image.open("Tests/images/clipboard.dib") as im:
im.save(outfile) im.save(outfile)
@ -230,3 +224,13 @@ def test_offset() -> None:
# to exclude the palette size from the pixel data offset # to exclude the palette size from the pixel data offset
with Image.open("Tests/images/pal8_offset.bmp") as im: with Image.open("Tests/images/pal8_offset.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp")
def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
assert im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"]
assert im.mode == "RGB"
monkeypatch.setattr(BmpImagePlugin, "USE_RAW_ALPHA", True)
with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
assert im.mode == "RGBA"

View File

@ -43,7 +43,7 @@ def test_load() -> None:
def test_save(tmp_path: Path) -> None: def test_save(tmp_path: Path) -> None:
# Arrange # Arrange
im = hopper() im = hopper()
tmpfile = str(tmp_path / "temp.bufr") tmpfile = tmp_path / "temp.bufr"
# Act / Assert: stub cannot save without an implemented handler # Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None:
im.load() im.load()
assert handler.is_loaded() assert handler.is_loaded()
temp_file = str(tmp_path / "temp.bufr") temp_file = tmp_path / "temp.bufr"
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved

View File

@ -26,12 +26,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None: def test_unclosed_file() -> None:
def open() -> None: def open_test_image() -> None:
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
with pytest.warns(ResourceWarning): with pytest.warns(ResourceWarning):
open() open_test_image()
def test_closed_file() -> None: def test_closed_file() -> None:
@ -69,12 +69,14 @@ def test_tell() -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert isinstance(im, DcxImagePlugin.DcxImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert isinstance(im, DcxImagePlugin.DcxImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame

View File

@ -9,7 +9,13 @@ import pytest
from PIL import DdsImagePlugin, Image from PIL import DdsImagePlugin, Image
from .helper import assert_image_equal, assert_image_equal_tofile, hopper from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
assert_image_similar_tofile,
hopper,
)
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
@ -109,6 +115,32 @@ def test_sanity_ati1_bc4u(image_path: str) -> None:
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
def test_dx10_bc2(tmp_path: Path) -> None:
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT3) as im:
im.save(out, pixel_format="BC2")
with Image.open(out) as reloaded:
assert reloaded.format == "DDS"
assert reloaded.mode == "RGBA"
assert reloaded.size == (256, 256)
assert_image_similar(im, reloaded, 3.81)
def test_dx10_bc3(tmp_path: Path) -> None:
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT5) as im:
im.save(out, pixel_format="BC3")
with Image.open(out) as reloaded:
assert reloaded.format == "DDS"
assert reloaded.mode == "RGBA"
assert reloaded.size == (256, 256)
assert_image_similar(im, reloaded, 3.69)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"image_path", "image_path",
( (
@ -331,11 +363,13 @@ def test_dxt5_colorblock_alpha_issue_4142() -> None:
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
px = im.getpixel((0, 0)) px = im.getpixel((0, 0))
assert isinstance(px, tuple)
assert px[0] != 0 assert px[0] != 0
assert px[1] != 0 assert px[1] != 0
assert px[2] != 0 assert px[2] != 0
px = im.getpixel((1, 0)) px = im.getpixel((1, 0))
assert isinstance(px, tuple)
assert px[0] != 0 assert px[0] != 0
assert px[1] != 0 assert px[1] != 0
assert px[2] != 0 assert px[2] != 0
@ -366,9 +400,9 @@ def test_not_implemented(test_file: str) -> None:
def test_save_unsupported_mode(tmp_path: Path) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
im = hopper("HSV") im = hopper("HSV")
with pytest.raises(OSError): with pytest.raises(OSError, match="cannot write mode HSV as DDS"):
im.save(out) im.save(out)
@ -382,10 +416,115 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
], ],
) )
def test_save(mode: str, test_file: str, tmp_path: Path) -> None: def test_save(mode: str, test_file: str, tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds") out = tmp_path / "temp.dds"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.mode == mode assert im.mode == mode
im.save(out) im.save(out)
assert_image_equal_tofile(im, out)
def test_save_unsupported_pixel_format(tmp_path: Path) -> None:
out = tmp_path / "temp.dds"
im = hopper()
with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"):
im.save(out, pixel_format="UNKNOWN")
def test_save_dxt1(tmp_path: Path) -> None:
# RGB
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT1) as im:
im.convert("RGB").save(out, pixel_format="DXT1")
assert_image_similar_tofile(im, out, 1.84)
# RGBA
im_alpha = im.copy()
im_alpha.putpixel((0, 0), (0, 0, 0, 0))
im_alpha.save(out, pixel_format="DXT1")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert_image_equal(im, reloaded) assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0)
# L
im_l = im.convert("L")
im_l.save(out, pixel_format="DXT1")
assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07)
# LA
im_alpha.convert("LA").save(out, pixel_format="DXT1")
with Image.open(out) as reloaded:
assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0)
def test_save_dxt3(tmp_path: Path) -> None:
# RGB
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT3) as im:
im_rgb = im.convert("RGB")
im_rgb.save(out, pixel_format="DXT3")
assert_image_similar_tofile(im_rgb.convert("RGBA"), out, 1.26)
# RGBA
im.save(out, pixel_format="DXT3")
assert_image_similar_tofile(im, out, 3.81)
# L
im_l = im.convert("L")
im_l.save(out, pixel_format="DXT3")
assert_image_similar_tofile(im_l.convert("RGBA"), out, 5.89)
# LA
im_la = im.convert("LA")
im_la.save(out, pixel_format="DXT3")
assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.44)
def test_save_dxt5(tmp_path: Path) -> None:
# RGB
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DXT1) as im:
im.convert("RGB").save(out, pixel_format="DXT5")
assert_image_similar_tofile(im, out, 1.84)
# RGBA
with Image.open(TEST_FILE_DXT5) as im_rgba:
im_rgba.save(out, pixel_format="DXT5")
assert_image_similar_tofile(im_rgba, out, 3.69)
# L
im_l = im.convert("L")
im_l.save(out, pixel_format="DXT5")
assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07)
# LA
im_la = im_rgba.convert("LA")
im_la.save(out, pixel_format="DXT5")
assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.32)
def test_save_dx10_bc5(tmp_path: Path) -> None:
out = tmp_path / "temp.dds"
with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im:
im.save(out, pixel_format="BC5")
assert_image_similar_tofile(im, out, 9.56)
im = hopper("L")
with pytest.raises(OSError, match="only RGB mode can be written as 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

@ -15,6 +15,7 @@ from .helper import (
is_win32, is_win32,
mark_if_feature_version, mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
timeout_unless_slower_valgrind,
) )
HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript()
@ -86,6 +87,8 @@ simple_eps_file_with_long_binary_data = (
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
expected_size = tuple(s * scale for s in size) expected_size = tuple(s * scale for s in size)
with Image.open(filename) as image: with Image.open(filename) as image:
assert isinstance(image, EpsImagePlugin.EpsImageFile)
image.load(scale=scale) image.load(scale=scale)
assert image.mode == "RGB" assert image.mode == "RGB"
assert image.size == expected_size assert image.size == expected_size
@ -95,10 +98,14 @@ def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_load() -> None: def test_load() -> None:
with Image.open(FILE1) as im: with Image.open(FILE1) as im:
assert im.load()[0, 0] == (255, 255, 255) px = im.load()
assert px is not None
assert px[0, 0] == (255, 255, 255)
# Test again now that it has already been loaded once # Test again now that it has already been loaded once
assert im.load()[0, 0] == (255, 255, 255) px = im.load()
assert px is not None
assert px[0, 0] == (255, 255, 255)
def test_binary() -> None: def test_binary() -> None:
@ -223,6 +230,8 @@ def test_showpage() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_transparency() -> None: def test_transparency() -> None:
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
assert isinstance(plot_image, EpsImagePlugin.EpsImageFile)
plot_image.load(transparency=True) plot_image.load(transparency=True)
assert plot_image.mode == "RGBA" assert plot_image.mode == "RGBA"
@ -235,7 +244,7 @@ def test_transparency() -> None:
def test_file_object(tmp_path: Path) -> None: def test_file_object(tmp_path: Path) -> None:
# issue 479 # issue 479
with Image.open(FILE1) as image1: with Image.open(FILE1) as image1:
with open(str(tmp_path / "temp.eps"), "wb") as fh: with open(tmp_path / "temp.eps", "wb") as fh:
image1.save(fh, "EPS") image1.save(fh, "EPS")
@ -270,7 +279,7 @@ def test_1(filename: str) -> None:
def test_image_mode_not_supported(tmp_path: Path) -> None: def test_image_mode_not_supported(tmp_path: Path) -> None:
im = hopper("RGBA") im = hopper("RGBA")
tmpfile = str(tmp_path / "temp.eps") tmpfile = tmp_path / "temp.eps"
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(tmpfile) im.save(tmpfile)
@ -304,6 +313,7 @@ def test_render_scale2() -> None:
# Zero bounding box # Zero bounding box
with Image.open(FILE1) as image1_scale2: with Image.open(FILE1) as image1_scale2:
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
image1_scale2.load(scale=2) image1_scale2.load(scale=2)
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
image1_scale2_compare = image1_scale2_compare.convert("RGB") image1_scale2_compare = image1_scale2_compare.convert("RGB")
@ -312,6 +322,7 @@ def test_render_scale2() -> None:
# Non-zero bounding box # Non-zero bounding box
with Image.open(FILE2) as image2_scale2: with Image.open(FILE2) as image2_scale2:
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
image2_scale2.load(scale=2) image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
image2_scale2_compare = image2_scale2_compare.convert("RGB") image2_scale2_compare = image2_scale2_compare.convert("RGB")
@ -388,7 +399,7 @@ def test_emptyline() -> None:
assert image.format == "EPS" assert image.format == "EPS"
@pytest.mark.timeout(timeout=5) @timeout_unless_slower_valgrind(5)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file",
["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],

View File

@ -1,12 +1,18 @@
from __future__ import annotations from __future__ import annotations
import io
import warnings import warnings
import pytest import pytest
from PIL import FliImagePlugin, Image, ImageFile from PIL import FliImagePlugin, Image, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy from .helper import (
assert_image_equal,
assert_image_equal_tofile,
is_pypy,
timeout_unless_slower_valgrind,
)
# created as an export of a palette image from Gimp2.6 # created as an export of a palette image from Gimp2.6
# save as...-> hopper.fli, default options. # save as...-> hopper.fli, default options.
@ -21,6 +27,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
def test_sanity() -> None: def test_sanity() -> None:
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
im.load() im.load()
assert im.mode == "P" assert im.mode == "P"
assert im.size == (128, 128) assert im.size == (128, 128)
@ -28,6 +36,8 @@ def test_sanity() -> None:
assert not im.is_animated assert not im.is_animated
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) 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"
@ -52,12 +62,12 @@ def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None: def test_unclosed_file() -> None:
def open() -> None: def open_test_image() -> None:
im = Image.open(static_test_file) im = Image.open(static_test_file)
im.load() im.load()
with pytest.warns(ResourceWarning): with pytest.warns(ResourceWarning):
open() open_test_image()
def test_closed_file() -> None: def test_closed_file() -> None:
@ -111,16 +121,19 @@ def test_palette_chunk_second() -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.n_frames == 384 assert im.n_frames == 384
assert im.is_animated assert im.is_animated
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame
@ -132,6 +145,15 @@ def test_eoferror() -> None:
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_missing_frame_size() -> None:
with open(animated_test_file, "rb") as fp:
data = fp.read()
data = data[:6188]
with Image.open(io.BytesIO(data)) as im:
with pytest.raises(EOFError, match="missing frame size"):
im.seek(1)
def test_seek_tell() -> None: def test_seek_tell() -> None:
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
layer_number = im.tell() layer_number = im.tell()
@ -156,10 +178,14 @@ def test_seek_tell() -> None:
def test_seek() -> None: def test_seek() -> None:
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
im.seek(50) im.seek(50)
assert_image_equal_tofile(im, "Tests/images/a_fli.png") assert_image_equal_tofile(im, "Tests/images/a_fli.png")
with pytest.raises(ValueError, match="cannot seek to frame 52"):
im._seek(52)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file",
@ -168,7 +194,7 @@ def test_seek() -> None:
"Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli",
], ],
) )
@pytest.mark.timeout(timeout=3) @timeout_unless_slower_valgrind(3)
def test_timeouts(test_file: str) -> None: def test_timeouts(test_file: str) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
with Image.open(f) as im: with Image.open(f) as im:

View File

@ -22,10 +22,11 @@ def test_sanity() -> None:
def test_close() -> None: def test_close() -> None:
with Image.open("Tests/images/input_bw_one_band.fpx") as im: with Image.open("Tests/images/input_bw_one_band.fpx") as im:
pass assert isinstance(im, FpxImagePlugin.FpxImageFile)
assert im.ole.fp.closed assert im.ole.fp.closed
im = Image.open("Tests/images/input_bw_one_band.fpx") im = Image.open("Tests/images/input_bw_one_band.fpx")
assert isinstance(im, FpxImagePlugin.FpxImageFile)
im.close() im.close()
assert im.ole.fp.closed assert im.ole.fp.closed

View File

@ -1,5 +1,8 @@
from __future__ import annotations from __future__ import annotations
import io
import struct
import pytest import pytest
from PIL import FtexImagePlugin, Image from PIL import FtexImagePlugin, Image
@ -23,3 +26,15 @@ def test_invalid_file() -> None:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
FtexImagePlugin.FtexImageFile(invalid_file) FtexImagePlugin.FtexImageFile(invalid_file)
def test_invalid_texture() -> None:
with open("Tests/images/ftex_dxt1.ftc", "rb") as fp:
data = fp.read()
# Change texture compression format
data = data[:24] + struct.pack("<i", 2) + data[28:]
with pytest.raises(ValueError, match="Invalid texture compression format: 2"):
with Image.open(io.BytesIO(data)):
pass

View File

@ -14,10 +14,14 @@ def test_gbr_file() -> None:
def test_load() -> None: def test_load() -> None:
with Image.open("Tests/images/gbr.gbr") as im: with Image.open("Tests/images/gbr.gbr") as im:
assert im.load()[0, 0] == (0, 0, 0, 0) px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
# Test again now that it has already been loaded once # Test again now that it has already been loaded once
assert im.load()[0, 0] == (0, 0, 0, 0) px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
def test_multiple_load_operations() -> None: def test_multiple_load_operations() -> None:

View File

@ -4,6 +4,8 @@ import pytest
from PIL import GdImageFile, UnidentifiedImageError from PIL import GdImageFile, UnidentifiedImageError
from .helper import assert_image_similar_tofile
TEST_GD_FILE = "Tests/images/hopper.gd" TEST_GD_FILE = "Tests/images/hopper.gd"
@ -11,6 +13,7 @@ def test_sanity() -> None:
with GdImageFile.open(TEST_GD_FILE) as im: with GdImageFile.open(TEST_GD_FILE) as im:
assert im.size == (128, 128) assert im.size == (128, 128)
assert im.format == "GD" assert im.format == "GD"
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14)
def test_bad_mode() -> None: def test_bad_mode() -> None:

View File

@ -22,9 +22,6 @@ from .helper import (
# sample gif stream # sample gif stream
TEST_GIF = "Tests/images/hopper.gif" TEST_GIF = "Tests/images/hopper.gif"
with open(TEST_GIF, "rb") as f:
data = f.read()
def test_sanity() -> None: def test_sanity() -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
@ -37,12 +34,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None: def test_unclosed_file() -> None:
def open() -> None: def open_test_image() -> None:
im = Image.open(TEST_GIF) im = Image.open(TEST_GIF)
im.load() im.load()
with pytest.warns(ResourceWarning): with pytest.warns(ResourceWarning):
open() open_test_image()
def test_closed_file() -> None: def test_closed_file() -> None:
@ -227,11 +224,12 @@ 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
def test_full_palette_second_frame(tmp_path: Path) -> None: def test_full_palette_second_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("P", (1, 256)) im = Image.new("P", (1, 256))
full_palette_im = Image.new("P", (1, 256)) full_palette_im = Image.new("P", (1, 256))
@ -252,7 +250,7 @@ def test_full_palette_second_frame(tmp_path: Path) -> None:
def test_roundtrip(tmp_path: Path) -> None: def test_roundtrip(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = hopper() im = hopper()
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -261,7 +259,7 @@ def test_roundtrip(tmp_path: Path) -> None:
def test_roundtrip2(tmp_path: Path) -> None: def test_roundtrip2(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/403 # see https://github.com/python-pillow/Pillow/issues/403
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im2 = im.copy() im2 = im.copy()
im2.save(out) im2.save(out)
@ -271,7 +269,7 @@ def test_roundtrip2(tmp_path: Path) -> None:
def test_roundtrip_save_all(tmp_path: Path) -> None: def test_roundtrip_save_all(tmp_path: Path) -> None:
# Single frame image # Single frame image
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = hopper() im = hopper()
im.save(out, save_all=True) im.save(out, save_all=True)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -279,7 +277,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
# Multiframe image # Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out, save_all=True) im.save(out, save_all=True)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -287,7 +285,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
def test_roundtrip_save_all_1(tmp_path: Path) -> None: def test_roundtrip_save_all_1(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("1", (1, 1)) im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1) im2 = Image.new("1", (1, 1), 1)
im.save(out, save_all=True, append_images=[im2]) im.save(out, save_all=True, append_images=[im2])
@ -310,6 +308,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
def test_loading_multiple_palettes(path: str, mode: str) -> None: def test_loading_multiple_palettes(path: str, mode: str) -> None:
with Image.open(path) as im: with Image.open(path) as im:
assert im.mode == "P" assert im.mode == "P"
assert im.palette is not None
first_frame_colors = im.palette.colors.keys() first_frame_colors = im.palette.colors.keys()
original_color = im.convert("RGB").getpixel((0, 0)) original_color = im.convert("RGB").getpixel((0, 0))
@ -331,7 +330,7 @@ def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
info = im.info.copy() info = im.info.copy()
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out, save_all=True) im.save(out, save_all=True)
with Image.open(out) as reread: with Image.open(out) as reread:
for header in important_headers: for header in important_headers:
@ -347,7 +346,7 @@ def test_palette_handling(tmp_path: Path) -> None:
im = im.resize((100, 100), Image.Resampling.LANCZOS) im = im.resize((100, 100), Image.Resampling.LANCZOS)
im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
f = str(tmp_path / "temp.gif") f = tmp_path / "temp.gif"
im2.save(f, optimize=True) im2.save(f, optimize=True)
with Image.open(f) as reloaded: with Image.open(f) as reloaded:
@ -358,7 +357,7 @@ def test_palette_434(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/434 # see https://github.com/python-pillow/Pillow/issues/434
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.copy().save(out, "GIF", **kwargs) im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out) reloaded = Image.open(out)
@ -404,6 +403,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
def test_seek() -> None: def test_seek() -> None:
with Image.open("Tests/images/dispose_none.gif") as img: with Image.open("Tests/images/dispose_none.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
frame_count = 0 frame_count = 0
try: try:
while True: while True:
@ -412,6 +412,10 @@ def test_seek() -> None:
except EOFError: except EOFError:
assert frame_count == 5 assert frame_count == 5
img.seek(0)
with pytest.raises(ValueError, match="cannot seek to frame 2"):
img._seek(2)
def test_seek_info() -> None: def test_seek_info() -> None:
with Image.open("Tests/images/iss634.gif") as im: with Image.open("Tests/images/iss634.gif") as im:
@ -444,10 +448,12 @@ def test_seek_rewind() -> None:
def test_n_frames(path: str, n_frames: int) -> None: def test_n_frames(path: str, n_frames: int) -> None:
# Test is_animated before n_frames # Test is_animated before n_frames
with Image.open(path) as im: with Image.open(path) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.is_animated == (n_frames != 1) assert im.is_animated == (n_frames != 1)
# Test is_animated after n_frames # Test is_animated after n_frames
with Image.open(path) as im: with Image.open(path) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == n_frames assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1) assert im.is_animated == (n_frames != 1)
@ -457,6 +463,7 @@ def test_no_change() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(1) im.seek(1)
expected = im.copy() expected = im.copy()
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == 5 assert im.n_frames == 5
assert_image_equal(im, expected) assert_image_equal(im, expected)
@ -464,17 +471,20 @@ def test_no_change() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(3) im.seek(3)
expected = im.copy() expected = im.copy()
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.is_animated assert im.is_animated
assert_image_equal(im, expected) assert_image_equal(im, expected)
with Image.open("Tests/images/comment_after_only_frame.gif") as im: with Image.open("Tests/images/comment_after_only_frame.gif") as im:
expected = Image.new("P", (1, 1)) expected = Image.new("P", (1, 1))
assert isinstance(im, GifImagePlugin.GifImageFile)
assert not im.is_animated assert not im.is_animated
assert_image_equal(im, expected) assert_image_equal(im, expected)
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame
@ -493,6 +503,7 @@ def test_first_frame_transparency() -> None:
def test_dispose_none() -> None: def test_dispose_none() -> None:
with Image.open("Tests/images/dispose_none.gif") as img: with Image.open("Tests/images/dispose_none.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try: try:
while True: while True:
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
@ -516,6 +527,7 @@ def test_dispose_none_load_end() -> None:
def test_dispose_background() -> None: def test_dispose_background() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as img: with Image.open("Tests/images/dispose_bgnd.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try: try:
while True: while True:
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
@ -528,7 +540,10 @@ def test_dispose_background_transparency() -> None:
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
img.seek(2) img.seek(2)
px = img.load() px = img.load()
assert px[35, 30][3] == 0 assert px is not None
value = px[35, 30]
assert isinstance(value, tuple)
assert value[3] == 0
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -568,6 +583,7 @@ def test_transparent_dispose(
def test_dispose_previous() -> None: def test_dispose_previous() -> None:
with Image.open("Tests/images/dispose_prev.gif") as img: with Image.open("Tests/images/dispose_prev.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try: try:
while True: while True:
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
@ -596,15 +612,16 @@ def test_previous_frame_loaded() -> None:
def test_save_dispose(tmp_path: Path) -> None: def test_save_dispose(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#111"), Image.new("L", (100, 100), "#111"),
Image.new("L", (100, 100), "#222"), Image.new("L", (100, 100), "#222"),
] ]
for method in range(0, 4): for method in range(4):
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
with Image.open(out) as img: with Image.open(out) as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
for _ in range(2): for _ in range(2):
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
assert img.disposal_method == method assert img.disposal_method == method
@ -618,13 +635,14 @@ def test_save_dispose(tmp_path: Path) -> None:
) )
with Image.open(out) as img: with Image.open(out) as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
for i in range(2): for i in range(2):
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
assert img.disposal_method == i + 1 assert img.disposal_method == i + 1
def test_dispose2_palette(tmp_path: Path) -> None: def test_dispose2_palette(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
# Four colors: white, gray, black, red # Four colors: white, gray, black, red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
@ -658,7 +676,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
def test_dispose2_diff(tmp_path: Path) -> None: def test_dispose2_diff(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
# 4 frames: red/blue, red/red, blue/blue, red/blue # 4 frames: red/blue, red/red, blue/blue, red/blue
circles = [ circles = [
@ -700,7 +718,7 @@ def test_dispose2_diff(tmp_path: Path) -> None:
def test_dispose2_background(tmp_path: Path) -> None: def test_dispose2_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [] im_list = []
@ -726,7 +744,7 @@ def test_dispose2_background(tmp_path: Path) -> None:
def test_dispose2_background_frame(tmp_path: Path) -> None: def test_dispose2_background_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [Image.new("RGBA", (1, 20))] im_list = [Image.new("RGBA", (1, 20))]
@ -740,11 +758,12 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
with Image.open(out) as im: with Image.open(out) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == 3 assert im.n_frames == 3
def test_dispose2_previous_frame(tmp_path: Path) -> None: def test_dispose2_previous_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("P", (100, 100)) im = Image.new("P", (100, 100))
im.info["transparency"] = 0 im.info["transparency"] = 0
@ -762,8 +781,23 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None:
assert im.getpixel((0, 0)) == (0, 0, 0, 255) assert im.getpixel((0, 0)) == (0, 0, 0, 255)
def test_dispose2_without_transparency(tmp_path: Path) -> None:
out = tmp_path / "temp.gif"
im = Image.new("P", (100, 100))
im2 = Image.new("P", (100, 100), (0, 0, 0))
im2.putpixel((50, 50), (255, 0, 0))
im.save(out, save_all=True, append_images=[im2], disposal=2)
with Image.open(out) as reloaded:
reloaded.seek(1)
assert reloaded.tile[0].extents == (0, 0, 100, 100)
def test_transparency_in_second_frame(tmp_path: Path) -> None: def test_transparency_in_second_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
with Image.open("Tests/images/different_transparency.gif") as im: with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0 assert im.info["transparency"] == 0
@ -793,7 +827,7 @@ def test_no_transparency_in_second_frame() -> None:
def test_remapped_transparency(tmp_path: Path) -> None: def test_remapped_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("P", (1, 2)) im = Image.new("P", (1, 2))
im2 = im.copy() im2 = im.copy()
@ -811,7 +845,7 @@ def test_remapped_transparency(tmp_path: Path) -> None:
def test_duration(tmp_path: Path) -> None: def test_duration(tmp_path: Path) -> None:
duration = 1000 duration = 1000
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
# Check that the argument has priority over the info settings # Check that the argument has priority over the info settings
@ -825,7 +859,7 @@ def test_duration(tmp_path: Path) -> None:
def test_multiple_duration(tmp_path: Path) -> None: def test_multiple_duration(tmp_path: Path) -> None:
duration_list = [1000, 2000, 3000] duration_list = [1000, 2000, 3000]
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#111"), Image.new("L", (100, 100), "#111"),
@ -860,7 +894,7 @@ def test_multiple_duration(tmp_path: Path) -> None:
def test_roundtrip_info_duration(tmp_path: Path) -> None: def test_roundtrip_info_duration(tmp_path: Path) -> None:
duration_list = [100, 500, 500] duration_list = [100, 500, 500]
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
with Image.open("Tests/images/transparent_dispose.gif") as im: with Image.open("Tests/images/transparent_dispose.gif") as im:
assert [ assert [
frame.info["duration"] for frame in ImageSequence.Iterator(im) frame.info["duration"] for frame in ImageSequence.Iterator(im)
@ -875,7 +909,7 @@ def test_roundtrip_info_duration(tmp_path: Path) -> None:
def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
with Image.open("Tests/images/duplicate_frame.gif") as im: with Image.open("Tests/images/duplicate_frame.gif") as im:
assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
1000, 1000,
@ -893,7 +927,7 @@ def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
def test_identical_frames(tmp_path: Path) -> None: def test_identical_frames(tmp_path: Path) -> None:
duration_list = [1000, 1500, 2000, 4000] duration_list = [1000, 1500, 2000, 4000]
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
@ -906,6 +940,8 @@ def test_identical_frames(tmp_path: Path) -> None:
out, save_all=True, append_images=im_list[1:], duration=duration_list out, save_all=True, append_images=im_list[1:], duration=duration_list
) )
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
# Assert that the first three frames were combined # Assert that the first three frames were combined
assert reread.n_frames == 2 assert reread.n_frames == 2
@ -926,7 +962,7 @@ def test_identical_frames(tmp_path: Path) -> None:
def test_identical_frames_to_single_frame( def test_identical_frames_to_single_frame(
duration: int | list[int], tmp_path: Path duration: int | list[int], tmp_path: Path
) -> None: ) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
@ -935,6 +971,8 @@ def test_identical_frames_to_single_frame(
im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
# Assert that all frames were combined # Assert that all frames were combined
assert reread.n_frames == 1 assert reread.n_frames == 1
@ -943,7 +981,7 @@ def test_identical_frames_to_single_frame(
def test_loop_none(tmp_path: Path) -> None: def test_loop_none(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.save(out, loop=None) im.save(out, loop=None)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -953,7 +991,7 @@ def test_loop_none(tmp_path: Path) -> None:
def test_number_of_loops(tmp_path: Path) -> None: def test_number_of_loops(tmp_path: Path) -> None:
number_of_loops = 2 number_of_loops = 2
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.save(out, loop=number_of_loops) im.save(out, loop=number_of_loops)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -969,7 +1007,7 @@ def test_number_of_loops(tmp_path: Path) -> None:
def test_background(tmp_path: Path) -> None: def test_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.info["background"] = 1 im.info["background"] = 1
im.save(out) im.save(out)
@ -978,7 +1016,7 @@ def test_background(tmp_path: Path) -> None:
def test_webp_background(tmp_path: Path) -> None: def test_webp_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
# Test opaque WebP background # Test opaque WebP background
if features.check("webp"): if features.check("webp"):
@ -996,7 +1034,7 @@ def test_comment(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0"
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.info["comment"] = b"Test comment text" im.info["comment"] = b"Test comment text"
im.save(out) im.save(out)
@ -1013,7 +1051,7 @@ def test_comment(tmp_path: Path) -> None:
def test_comment_over_255(tmp_path: Path) -> None: def test_comment_over_255(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
comment = b"Test comment text" comment = b"Test comment text"
while len(comment) < 256: while len(comment) < 256:
@ -1039,7 +1077,7 @@ def test_read_multiple_comment_blocks() -> None:
def test_empty_string_comment(tmp_path: Path) -> None: def test_empty_string_comment(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
assert "comment" in im.info assert "comment" in im.info
@ -1073,7 +1111,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
assert "comment" not in im.info assert "comment" not in im.info
# Test that a saved image keeps the comment # Test that a saved image keeps the comment
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
with Image.open("Tests/images/dispose_prev.gif") as im: with Image.open("Tests/images/dispose_prev.gif") as im:
im.save(out, save_all=True, comment="Test") im.save(out, save_all=True, comment="Test")
@ -1083,7 +1121,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
def test_version(tmp_path: Path) -> None: def test_version(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
def assert_version_after_save(im: Image.Image, version: bytes) -> None: def assert_version_after_save(im: Image.Image, version: bytes) -> None:
im.save(out) im.save(out)
@ -1113,7 +1151,7 @@ def test_version(tmp_path: Path) -> None:
def test_append_images(tmp_path: Path) -> None: def test_append_images(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
# Test appending single frame images # Test appending single frame images
im = Image.new("RGB", (100, 100), "#f00") im = Image.new("RGB", (100, 100), "#f00")
@ -1121,6 +1159,14 @@ def test_append_images(tmp_path: Path) -> None:
im.copy().save(out, save_all=True, append_images=ims) im.copy().save(out, save_all=True, append_images=ims)
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3
# Test append_images without save_all
im.copy().save(out, append_images=ims)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3 assert reread.n_frames == 3
# Tests appending using a generator # Tests appending using a generator
@ -1130,6 +1176,7 @@ def test_append_images(tmp_path: Path) -> None:
im.save(out, save_all=True, append_images=im_generator(ims)) im.save(out, save_all=True, append_images=im_generator(ims))
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3 assert reread.n_frames == 3
# Tests appending single and multiple frame images # Tests appending single and multiple frame images
@ -1138,11 +1185,12 @@ def test_append_images(tmp_path: Path) -> None:
im.save(out, save_all=True, append_images=[im2]) im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 10 assert reread.n_frames == 10
def test_append_different_size_image(tmp_path: Path) -> None: def test_append_different_size_image(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("RGB", (100, 100)) im = Image.new("RGB", (100, 100))
bigger_im = Image.new("RGB", (200, 200), "#f00") bigger_im = Image.new("RGB", (200, 200), "#f00")
@ -1169,7 +1217,7 @@ def test_transparent_optimize(tmp_path: Path) -> None:
im.frombytes(data) im.frombytes(data)
im.putpalette(palette) im.putpalette(palette)
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out, transparency=im.getpixel((252, 0))) im.save(out, transparency=im.getpixel((252, 0)))
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1177,14 +1225,16 @@ def test_transparent_optimize(tmp_path: Path) -> None:
def test_removed_transparency(tmp_path: Path) -> None: def test_removed_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("RGB", (256, 1)) im = Image.new("RGB", (256, 1))
for x in range(256): for x in range(256):
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:
@ -1192,7 +1242,7 @@ def test_removed_transparency(tmp_path: Path) -> None:
def test_rgb_transparency(tmp_path: Path) -> None: def test_rgb_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
# Single frame # Single frame
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
@ -1206,7 +1256,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:
@ -1214,7 +1264,7 @@ def test_rgb_transparency(tmp_path: Path) -> None:
def test_rgba_transparency(tmp_path: Path) -> None: def test_rgba_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = hopper("P") im = hopper("P")
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
@ -1224,25 +1274,26 @@ def test_rgba_transparency(tmp_path: Path) -> None:
assert_image_equal(hopper("P").convert("RGB"), reloaded) assert_image_equal(hopper("P").convert("RGB"), reloaded)
def test_background_outside_palettte(tmp_path: Path) -> None: def test_background_outside_palettte() -> None:
with Image.open("Tests/images/background_outside_palette.gif") as im: with Image.open("Tests/images/background_outside_palette.gif") as im:
im.seek(1) im.seek(1)
assert im.info["background"] == 255 assert im.info["background"] == 255
def test_bbox(tmp_path: Path) -> None: def test_bbox(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("RGB", (100, 100), "#fff") im = Image.new("RGB", (100, 100), "#fff")
ims = [Image.new("RGB", (100, 100), "#000")] ims = [Image.new("RGB", (100, 100), "#000")]
im.save(out, save_all=True, append_images=ims) im.save(out, save_all=True, append_images=ims)
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 2 assert reread.n_frames == 2
def test_bbox_alpha(tmp_path: Path) -> None: def test_bbox_alpha(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) im = Image.new("RGBA", (1, 2), (255, 0, 0, 255))
im.putpixel((0, 1), (255, 0, 0, 0)) im.putpixel((0, 1), (255, 0, 0, 0))
@ -1250,6 +1301,7 @@ def test_bbox_alpha(tmp_path: Path) -> None:
im.save(out, save_all=True, append_images=[im2]) im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 2 assert reread.n_frames == 2
@ -1261,7 +1313,7 @@ def test_palette_save_L(tmp_path: Path) -> None:
palette = im.getpalette() palette = im.getpalette()
assert palette is not None assert palette is not None
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im_l.save(out, palette=bytes(palette)) im_l.save(out, palette=bytes(palette))
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1272,7 +1324,7 @@ def test_palette_save_P(tmp_path: Path) -> None:
im = Image.new("P", (1, 2)) im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1) im.putpixel((0, 1), 1)
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out, palette=bytes((1, 2, 3, 4, 5, 6))) im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1288,7 +1340,7 @@ def test_palette_save_duplicate_entries(tmp_path: Path) -> None:
im.putpalette((0, 0, 0, 0, 0, 0)) im.putpalette((0, 0, 0, 0, 0, 0))
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1])
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1303,7 +1355,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
frame.putpalette(color) frame.putpalette(color)
frames.append(frame) frames.append(frame)
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
frames[0].save( frames[0].save(
out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]
) )
@ -1311,6 +1363,8 @@ 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_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.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)
@ -1325,7 +1379,7 @@ def test_palette_save_ImagePalette(tmp_path: Path) -> None:
im = hopper("P") im = hopper("P")
palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3)
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out, palette=palette) im.save(out, palette=palette)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -1338,24 +1392,24 @@ def test_save_I(tmp_path: Path) -> None:
im = hopper("I") im = hopper("I")
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im.save(out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert_image_equal(reloaded.convert("L"), im.convert("L")) assert_image_equal(reloaded.convert("L"), im.convert("L"))
def test_getdata() -> None: def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None:
# Test getheader/getdata against legacy values. # Test getheader/getdata against legacy values.
# Create a 'P' image with holes in the palette. # Create a 'P' image with holes in the palette.
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) im = Image.linear_gradient(mode="L").resize((16, 16), Image.Resampling.NEAREST)
im.putpalette(ImagePalette.ImagePalette("RGB")) im.putpalette(ImagePalette.ImagePalette("RGB"))
im.info = {"background": 0} im.info = {"background": 0}
passed_palette = bytes(255 - i // 3 for i in range(768)) passed_palette = bytes(255 - i // 3 for i in range(768))
GifImagePlugin._FORCE_OPTIMIZE = True monkeypatch.setattr(GifImagePlugin, "_FORCE_OPTIMIZE", True)
try:
h = GifImagePlugin.getheader(im, passed_palette) h = GifImagePlugin.getheader(im, passed_palette)
d = GifImagePlugin.getdata(im) d = GifImagePlugin.getdata(im)
@ -1369,14 +1423,14 @@ def test_getdata() -> None:
assert h == h_target assert h == h_target
assert d == d_target assert d == d_target
finally:
GifImagePlugin._FORCE_OPTIMIZE = False
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()
@ -1402,6 +1456,7 @@ def test_extents(
) -> None: ) -> None:
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
with Image.open("Tests/images/" + test_file) as im: with Image.open("Tests/images/" + test_file) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.size == (100, 100) assert im.size == (100, 100)
# Check that n_frames does not change the size # Check that n_frames does not change the size
@ -1424,18 +1479,22 @@ def test_missing_background() -> None:
def test_saving_rgba(tmp_path: Path) -> None: def test_saving_rgba(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
with Image.open("Tests/images/transparent.png") as im: with Image.open("Tests/images/transparent.png") as im:
im.save(out) im.save(out)
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}))
def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
out = str(tmp_path / "temp.gif") out = tmp_path / "temp.gif"
im1 = Image.new("P", (100, 100)) im1 = Image.new("P", (100, 100))
d = ImageDraw.Draw(im1) d = ImageDraw.Draw(im1)
@ -1449,4 +1508,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
im1.save(out, save_all=True, append_images=[im2], **params) im1.save(out, save_all=True, append_images=[im2], **params)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, GifImagePlugin.GifImageFile)
assert reloaded.n_frames == 2 assert reloaded.n_frames == 2

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
import pytest import pytest
from PIL.GimpPaletteFile import GimpPaletteFile from PIL.GimpPaletteFile import GimpPaletteFile
@ -14,17 +16,20 @@ def test_sanity() -> None:
GimpPaletteFile(fp) GimpPaletteFile(fp)
with open("Tests/images/bad_palette_file.gpl", "rb") as fp: with open("Tests/images/bad_palette_file.gpl", "rb") as fp:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError, match="bad palette file"):
GimpPaletteFile(fp) GimpPaletteFile(fp)
with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: with open("Tests/images/bad_palette_entry.gpl", "rb") as fp:
with pytest.raises(ValueError): with pytest.raises(ValueError, match="bad palette entry"):
GimpPaletteFile(fp) GimpPaletteFile(fp)
def test_get_palette() -> None: @pytest.mark.parametrize(
"filename, size", (("custom_gimp_palette.gpl", 8), ("full_gimp_palette.gpl", 256))
)
def test_get_palette(filename: str, size: int) -> None:
# Arrange # Arrange
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: with open("Tests/images/" + filename, "rb") as fp:
palette_file = GimpPaletteFile(fp) palette_file = GimpPaletteFile(fp)
# Act # Act
@ -32,3 +37,36 @@ def test_get_palette() -> None:
# Assert # Assert
assert mode == "RGB" assert mode == "RGB"
assert len(palette) / 3 == size
def test_frombytes() -> None:
# Test that __init__ stops reading after 260 lines
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
custom_data = fp.read()
custom_data += b"#\n" * 300 + b" 0 0 0 Index 12"
b = BytesIO(custom_data)
palette = GimpPaletteFile(b)
assert len(palette.palette) / 3 == 8
# Test that __init__ only reads 256 entries
with open("Tests/images/full_gimp_palette.gpl", "rb") as fp:
full_data = fp.read()
data = full_data.replace(b"#\n", b"") + b" 0 0 0 Index 256"
b = BytesIO(data)
palette = GimpPaletteFile(b)
assert len(palette.palette) / 3 == 256
# Test that frombytes() can read beyond that
palette = GimpPaletteFile.frombytes(data)
assert len(palette.palette) / 3 == 257
# Test that __init__ raises an error if a comment is too long
data = full_data[:-1] + b"a" * 100
b = BytesIO(data)
with pytest.raises(SyntaxError, match="bad palette file"):
palette = GimpPaletteFile(b)
# Test that frombytes() can read the data regardless
palette = GimpPaletteFile.frombytes(data)
assert len(palette.palette) / 3 == 256

View File

@ -43,7 +43,7 @@ def test_load() -> None:
def test_save(tmp_path: Path) -> None: def test_save(tmp_path: Path) -> None:
# Arrange # Arrange
im = hopper() im = hopper()
tmpfile = str(tmp_path / "temp.grib") tmpfile = tmp_path / "temp.grib"
# Act / Assert: stub cannot save without an implemented handler # Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None:
im.load() im.load()
assert handler.is_loaded() assert handler.is_loaded()
temp_file = str(tmp_path / "temp.grib") temp_file = tmp_path / "temp.grib"
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved

View File

@ -43,7 +43,7 @@ def test_save() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
dummy_fp = BytesIO() dummy_fp = BytesIO()
dummy_filename = "dummy.filename" dummy_filename = "dummy.h5"
# Act / Assert: stub cannot save without an implemented handler # Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
@ -81,7 +81,7 @@ def test_handler(tmp_path: Path) -> None:
im.load() im.load()
assert handler.is_loaded() assert handler.is_loaded()
temp_file = str(tmp_path / "temp.h5") temp_file = tmp_path / "temp.h5"
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved

View File

@ -32,14 +32,18 @@ def test_sanity() -> None:
def test_load() -> None: def test_load() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.load()[0, 0] == (0, 0, 0, 0) px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
# Test again now that it has already been loaded once # Test again now that it has already been loaded once
assert im.load()[0, 0] == (0, 0, 0, 0) px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
def test_save(tmp_path: Path) -> None: def test_save(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.icns") temp_file = tmp_path / "temp.icns"
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.save(temp_file) im.save(temp_file)
@ -56,7 +60,7 @@ def test_save(tmp_path: Path) -> None:
def test_save_append_images(tmp_path: Path) -> None: def test_save_append_images(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.icns") temp_file = tmp_path / "temp.icns"
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128))
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
@ -65,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None:
assert_image_similar_tofile(im, temp_file, 1) assert_image_similar_tofile(im, temp_file, 1)
with Image.open(temp_file) as reread: with Image.open(temp_file) as reread:
assert isinstance(reread, IcnsImagePlugin.IcnsImageFile)
reread.size = (16, 16) reread.size = (16, 16)
reread.load(2) reread.load(2)
assert_image_equal(reread, provided_im) assert_image_equal(reread, provided_im)
@ -86,10 +91,13 @@ def test_sizes() -> None:
# Check that we can load all of the sizes, and that the final pixel # Check that we can load all of the sizes, and that the final pixel
# dimensions are as expected # dimensions are as expected
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert isinstance(im, IcnsImagePlugin.IcnsImageFile)
for w, h, r in im.info["sizes"]: for w, h, r in im.info["sizes"]:
wr = w * r wr = w * r
hr = h * r hr = h * r
with pytest.warns(DeprecationWarning): with pytest.warns(
DeprecationWarning, match=r"Setting size to \(width, height, scale\)"
):
im.size = (w, h, r) im.size = (w, h, r)
im.load() im.load()
assert im.mode == "RGBA" assert im.mode == "RGBA"
@ -114,6 +122,7 @@ def test_older_icon() -> None:
wr = w * r wr = w * r
hr = h * r hr = h * r
with Image.open("Tests/images/pillow2.icns") as im2: with Image.open("Tests/images/pillow2.icns") as im2:
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
im2.size = (w, h) im2.size = (w, h)
im2.load(r) im2.load(r)
assert im2.mode == "RGBA" assert im2.mode == "RGBA"
@ -131,6 +140,7 @@ def test_jp2_icon() -> None:
wr = w * r wr = w * r
hr = h * r hr = h * r
with Image.open("Tests/images/pillow3.icns") as im2: with Image.open("Tests/images/pillow3.icns") as im2:
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
im2.size = (w, h) im2.size = (w, h)
im2.load(r) im2.load(r)
assert im2.mode == "RGBA" assert im2.mode == "RGBA"

View File

@ -24,7 +24,9 @@ def test_sanity() -> None:
def test_load() -> None: def test_load() -> None:
with Image.open(TEST_ICO_FILE) as im: with Image.open(TEST_ICO_FILE) as im:
assert im.load()[0, 0] == (1, 1, 9, 255) px = im.load()
assert px is not None
assert px[0, 0] == (1, 1, 9, 255)
def test_mask() -> None: def test_mask() -> None:
@ -39,7 +41,7 @@ def test_black_and_white() -> None:
def test_palette(tmp_path: Path) -> None: def test_palette(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico") temp_file = tmp_path / "temp.ico"
im = Image.new("P", (16, 16)) im = Image.new("P", (16, 16))
im.save(temp_file) im.save(temp_file)
@ -75,6 +77,7 @@ def test_save_to_bytes() -> None:
# The other one # The other one
output.seek(0) output.seek(0)
with Image.open(output) as reloaded: with Image.open(output) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.size = (32, 32) reloaded.size = (32, 32)
assert im.mode == reloaded.mode assert im.mode == reloaded.mode
@ -86,21 +89,23 @@ def test_save_to_bytes() -> None:
def test_getpixel(tmp_path: Path) -> None: def test_getpixel(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico") temp_file = tmp_path / "temp.ico"
im = hopper() im = hopper()
im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
with Image.open(temp_file) as reloaded: with Image.open(temp_file) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
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)
def test_no_duplicates(tmp_path: Path) -> None: def test_no_duplicates(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico") temp_file = tmp_path / "temp.ico"
temp_file2 = str(tmp_path / "temp2.ico") temp_file2 = tmp_path / "temp2.ico"
im = hopper() im = hopper()
sizes = [(32, 32), (64, 64)] sizes = [(32, 32), (64, 64)]
@ -113,8 +118,8 @@ def test_no_duplicates(tmp_path: Path) -> None:
def test_different_bit_depths(tmp_path: Path) -> None: def test_different_bit_depths(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico") temp_file = tmp_path / "temp.ico"
temp_file2 = str(tmp_path / "temp2.ico") temp_file2 = tmp_path / "temp2.ico"
im = hopper() im = hopper()
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
@ -130,8 +135,8 @@ def test_different_bit_depths(tmp_path: Path) -> None:
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
# Test that only matching sizes of different bit depths are saved # Test that only matching sizes of different bit depths are saved
temp_file3 = str(tmp_path / "temp3.ico") temp_file3 = tmp_path / "temp3.ico"
temp_file4 = str(tmp_path / "temp4.ico") temp_file4 = tmp_path / "temp4.ico"
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
im.save( im.save(
@ -165,6 +170,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
# The other one # The other one
output.seek(0) output.seek(0)
with Image.open(output) as reloaded: with Image.open(output) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.size = (32, 32) reloaded.size = (32, 32)
assert "RGBA" == reloaded.mode assert "RGBA" == reloaded.mode
@ -176,6 +182,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
def test_incorrect_size() -> None: def test_incorrect_size() -> None:
with Image.open(TEST_ICO_FILE) as im: with Image.open(TEST_ICO_FILE) as im:
assert isinstance(im, IcoImagePlugin.IcoImageFile)
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.size = (1, 1) im.size = (1, 1)
@ -184,7 +191,7 @@ def test_save_256x256(tmp_path: Path) -> None:
"""Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264"""
# Arrange # Arrange
with Image.open("Tests/images/hopper_256x256.ico") as im: with Image.open("Tests/images/hopper_256x256.ico") as im:
outfile = str(tmp_path / "temp_saved_hopper_256x256.ico") outfile = tmp_path / "temp_saved_hopper_256x256.ico"
# Act # Act
im.save(outfile) im.save(outfile)
@ -200,7 +207,7 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None:
""" """
# Arrange # Arrange
with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48 with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48
outfile = str(tmp_path / "temp_saved_python.ico") outfile = tmp_path / "temp_saved_python.ico"
# Act # Act
im.save(outfile) im.save(outfile)
@ -213,10 +220,11 @@ def test_save_append_images(tmp_path: Path) -> None:
# append_images should be used for scaled down versions of the image # append_images should be used for scaled down versions of the image
im = hopper("RGBA") im = hopper("RGBA")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) provided_im = Image.new("RGBA", (32, 32), (255, 0, 0))
outfile = str(tmp_path / "temp_saved_multi_icon.ico") outfile = tmp_path / "temp_saved_multi_icon.ico"
im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im])
with Image.open(outfile) as reread: with Image.open(outfile) as reread:
assert isinstance(reread, IcoImagePlugin.IcoImageFile)
assert_image_equal(reread, hopper("RGBA")) assert_image_equal(reread, hopper("RGBA"))
reread.size = (32, 32) reread.size = (32, 32)
@ -226,14 +234,14 @@ 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)
def test_draw_reloaded(tmp_path: Path) -> None: def test_draw_reloaded(tmp_path: Path) -> None:
with Image.open(TEST_ICO_FILE) as im: with Image.open(TEST_ICO_FILE) as im:
outfile = str(tmp_path / "temp_saved_hopper_draw.ico") outfile = tmp_path / "temp_saved_hopper_draw.ico"
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
draw.line((0, 0) + im.size, "#f00") draw.line((0, 0) + im.size, "#f00")

View File

@ -23,7 +23,7 @@ def test_sanity() -> None:
def test_name_limit(tmp_path: Path) -> None: def test_name_limit(tmp_path: Path) -> None:
out = str(tmp_path / ("name_limit_test" * 7 + ".im")) out = tmp_path / ("name_limit_test" * 7 + ".im")
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
im.save(out) im.save(out)
assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") assert filecmp.cmp(out, "Tests/images/hopper_long_name.im")
@ -31,12 +31,12 @@ def test_name_limit(tmp_path: Path) -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None: def test_unclosed_file() -> None:
def open() -> None: def open_test_image() -> None:
im = Image.open(TEST_IM) im = Image.open(TEST_IM)
im.load() im.load()
with pytest.warns(ResourceWarning): with pytest.warns(ResourceWarning):
open() open_test_image()
def test_closed_file() -> None: def test_closed_file() -> None:
@ -68,12 +68,14 @@ def test_tell() -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
assert isinstance(im, ImImagePlugin.ImImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
assert isinstance(im, ImImagePlugin.ImImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame
@ -87,7 +89,7 @@ def test_eoferror() -> None:
@pytest.mark.parametrize("mode", ("RGB", "P", "PA")) @pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
def test_roundtrip(mode: str, tmp_path: Path) -> None: def test_roundtrip(mode: str, tmp_path: Path) -> None:
out = str(tmp_path / "temp.im") out = tmp_path / "temp.im"
im = hopper(mode) im = hopper(mode)
im.save(out) im.save(out)
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
@ -98,7 +100,7 @@ def test_small_palette(tmp_path: Path) -> None:
colors = [0, 1, 2] colors = [0, 1, 2]
im.putpalette(colors) im.putpalette(colors)
out = str(tmp_path / "temp.im") out = tmp_path / "temp.im"
im.save(out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -106,7 +108,7 @@ def test_small_palette(tmp_path: Path) -> None:
def test_save_unsupported_mode(tmp_path: Path) -> None: def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.im") out = tmp_path / "temp.im"
im = hopper("HSV") im = hopper("HSV")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(out) im.save(out)

View File

@ -23,6 +23,9 @@ def test_open() -> None:
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
assert_image_equal(im, expected) assert_image_equal(im, expected)
with Image.open(f) as im:
assert im.load() is not None
def test_getiptcinfo_jpg_none() -> None: def test_getiptcinfo_jpg_none() -> None:
# Arrange # Arrange
@ -99,7 +102,7 @@ def test_i() -> None:
c = b"a" c = b"a"
# Act # Act
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="IptcImagePlugin.i"):
ret = IptcImagePlugin.i(c) ret = IptcImagePlugin.i(c)
# Assert # Assert
@ -114,7 +117,7 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(sys, "stdout", mystdout) monkeypatch.setattr(sys, "stdout", mystdout)
# Act # Act
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="IptcImagePlugin.dump"):
IptcImagePlugin.dump(c) IptcImagePlugin.dump(c)
# Assert # Assert
@ -122,5 +125,5 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
def test_pad_deprecation() -> None: def test_pad_deprecation() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="IptcImagePlugin.PAD"):
assert IptcImagePlugin.PAD == b"\0\0\0\0" assert IptcImagePlugin.PAD == b"\0\0\0\0"

View File

@ -32,6 +32,7 @@ from .helper import (
is_win32, is_win32,
mark_if_feature_version, mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
timeout_unless_slower_valgrind,
) )
ElementTree: ModuleType | None ElementTree: ModuleType | None
@ -83,7 +84,7 @@ class TestFileJpeg:
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
im = Image.new("RGB", size) im = Image.new("RGB", size)
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(f) im.save(f)
@ -91,6 +92,7 @@ class TestFileJpeg:
def test_app(self) -> None: def test_app(self) -> None:
# Test APP/COM reader (@PIL135) # Test APP/COM reader (@PIL135)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00")
assert im.applist[1] == ( assert im.applist[1] == (
"COM", "COM",
@ -128,30 +130,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, ...]:
@ -194,7 +192,7 @@ class TestFileJpeg:
icc_profile = im1.info["icc_profile"] icc_profile = im1.info["icc_profile"]
assert len(icc_profile) == 3144 assert len(icc_profile) == 3144
# Roundtrip via physical file. # Roundtrip via physical file.
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
im1.save(f, icc_profile=icc_profile) im1.save(f, icc_profile=icc_profile)
with Image.open(f) as im2: with Image.open(f) as im2:
assert im2.info.get("icc_profile") == icc_profile assert im2.info.get("icc_profile") == icc_profile
@ -238,7 +236,7 @@ class TestFileJpeg:
# Sometimes the meta data on the icc_profile block is bigger than # Sometimes the meta data on the icc_profile block is bigger than
# Image.MAXBLOCK or the image size. # Image.MAXBLOCK or the image size.
with Image.open("Tests/images/icc_profile_big.jpg") as im: with Image.open("Tests/images/icc_profile_big.jpg") as im:
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
icc_profile = im.info["icc_profile"] icc_profile = im.info["icc_profile"]
# Should not raise OSError for image with icc larger than image size. # Should not raise OSError for image with icc larger than image size.
im.save( im.save(
@ -250,11 +248,11 @@ class TestFileJpeg:
) )
with Image.open("Tests/images/flower2.jpg") as im: with Image.open("Tests/images/flower2.jpg") as im:
f = str(tmp_path / "temp2.jpg") f = tmp_path / "temp2.jpg"
im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955) im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955)
with Image.open("Tests/images/flower2.jpg") as im: with Image.open("Tests/images/flower2.jpg") as im:
f = str(tmp_path / "temp3.jpg") f = tmp_path / "temp3.jpg"
im.save(f, progressive=True, quality=94, exif=b" " * 43668) im.save(f, progressive=True, quality=94, exif=b" " * 43668)
def test_optimize(self) -> None: def test_optimize(self) -> None:
@ -268,7 +266,7 @@ class TestFileJpeg:
def test_optimize_large_buffer(self, tmp_path: Path) -> None: def test_optimize_large_buffer(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148 # https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
# this requires ~ 1.5x Image.MAXBLOCK # this requires ~ 1.5x Image.MAXBLOCK
im = Image.new("RGB", (4096, 4096), 0xFF3333) im = Image.new("RGB", (4096, 4096), 0xFF3333)
im.save(f, format="JPEG", optimize=True) im.save(f, format="JPEG", optimize=True)
@ -288,13 +286,13 @@ class TestFileJpeg:
assert im1_bytes >= im3_bytes assert im1_bytes >= im3_bytes
def test_progressive_large_buffer(self, tmp_path: Path) -> None: def test_progressive_large_buffer(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
# this requires ~ 1.5x Image.MAXBLOCK # this requires ~ 1.5x Image.MAXBLOCK
im = Image.new("RGB", (4096, 4096), 0xFF3333) im = Image.new("RGB", (4096, 4096), 0xFF3333)
im.save(f, format="JPEG", progressive=True) im.save(f, format="JPEG", progressive=True)
def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
im = self.gen_random_image((255, 255)) im = self.gen_random_image((255, 255))
# this requires more bytes than pixels in the image # this requires more bytes than pixels in the image
im.save(f, format="JPEG", progressive=True, quality=100) im.save(f, format="JPEG", progressive=True, quality=100)
@ -307,7 +305,7 @@ class TestFileJpeg:
def test_large_exif(self, tmp_path: Path) -> None: def test_large_exif(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148 # https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
im = hopper() im = hopper()
im.save(f, "JPEG", quality=90, exif=b"1" * 65533) im.save(f, "JPEG", quality=90, exif=b"1" * 65533)
@ -316,6 +314,8 @@ class TestFileJpeg:
def test_exif_typeerror(self) -> None: def test_exif_typeerror(self) -> None:
with Image.open("Tests/images/exif_typeerror.jpg") as im: with Image.open("Tests/images/exif_typeerror.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
# Should not raise a TypeError # Should not raise a TypeError
im._getexif() im._getexif()
@ -335,7 +335,7 @@ class TestFileJpeg:
assert exif[gps_index] == expected_exif_gps assert exif[gps_index] == expected_exif_gps
# Writing # Writing
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
exif = Image.Exif() exif = Image.Exif()
exif[gps_index] = expected_exif_gps exif[gps_index] = expected_exif_gps
hopper().save(f, exif=exif) hopper().save(f, exif=exif)
@ -500,20 +500,21 @@ class TestFileJpeg:
def test_mp(self) -> None: def test_mp(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)
assert im._getmp() is None assert im._getmp() is None
def test_quality_keep(self, tmp_path: Path) -> None: def test_quality_keep(self, tmp_path: Path) -> None:
# RGB # RGB
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
im.save(f, quality="keep") im.save(f, quality="keep")
# Grayscale # Grayscale
with Image.open("Tests/images/hopper_gray.jpg") as im: with Image.open("Tests/images/hopper_gray.jpg") as im:
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
im.save(f, quality="keep") im.save(f, quality="keep")
# CMYK # CMYK
with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: with Image.open("Tests/images/pil_sample_cmyk.jpg") as im:
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
im.save(f, quality="keep") im.save(f, quality="keep")
def test_junk_jpeg_header(self) -> None: def test_junk_jpeg_header(self) -> None:
@ -558,12 +559,14 @@ class TestFileJpeg:
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.save(b, "JPEG", qtables=[[n] * 64] * n) im.save(b, "JPEG", qtables=[[n] * 64] * n)
with Image.open(b) as im: with Image.open(b) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert len(im.quantization) == n assert len(im.quantization) == n
reloaded = self.roundtrip(im, qtables="keep") reloaded = self.roundtrip(im, qtables="keep")
assert im.quantization == reloaded.quantization assert im.quantization == reloaded.quantization
assert max(reloaded.quantization[0]) <= 255 assert max(reloaded.quantization[0]) <= 255
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
qtables = im.quantization qtables = im.quantization
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
assert im.quantization == reloaded.quantization assert im.quantization == reloaded.quantization
@ -663,6 +666,7 @@ class TestFileJpeg:
def test_load_16bit_qtables(self) -> None: def test_load_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)
assert len(im.quantization) == 2 assert len(im.quantization) == 2
assert len(im.quantization[0]) == 64 assert len(im.quantization[0]) == 64
assert max(im.quantization[0]) > 255 assert max(im.quantization[0]) > 255
@ -705,6 +709,7 @@ class TestFileJpeg:
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg(self) -> None: def test_load_djpeg(self) -> None:
with Image.open(TEST_FILE) as img: with Image.open(TEST_FILE) as img:
assert isinstance(img, JpegImagePlugin.JpegImageFile)
img.load_djpeg() img.load_djpeg()
assert_image_similar_tofile(img, TEST_FILE, 5) assert_image_similar_tofile(img, TEST_FILE, 5)
@ -726,7 +731,7 @@ class TestFileJpeg:
def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None:
im = self.gen_random_image((512, 512)) im = self.gen_random_image((512, 512))
f = str(tmp_path / "temp.jpeg") f = tmp_path / "temp.jpeg"
im.save(f, quality=100, optimize=True) im.save(f, quality=100, optimize=True)
with Image.open(f) as reloaded: with Image.open(f) as reloaded:
@ -741,11 +746,14 @@ 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
assert im.format == "JPEG" 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:
out = BytesIO() out = BytesIO()
@ -762,7 +770,7 @@ class TestFileJpeg:
def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: def test_save_tiff_with_dpi(self, tmp_path: Path) -> None:
# Arrange # Arrange
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
# Act # Act
im.save(outfile, "JPEG", dpi=im.info["dpi"]) im.save(outfile, "JPEG", dpi=im.info["dpi"])
@ -773,7 +781,7 @@ class TestFileJpeg:
assert im.info["dpi"] == reloaded.info["dpi"] assert im.info["dpi"] == reloaded.info["dpi"]
def test_save_dpi_rounding(self, tmp_path: Path) -> None: def test_save_dpi_rounding(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.jpg") outfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
im.save(outfile, dpi=(72.2, 72.2)) im.save(outfile, dpi=(72.2, 72.2))
@ -859,7 +867,7 @@ class TestFileJpeg:
exif = im.getexif() exif = im.getexif()
assert exif[282] == 180 assert exif[282] == 180
out = str(tmp_path / "out.jpg") out = tmp_path / "out.jpg"
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("error") warnings.simplefilter("error")
@ -909,6 +917,7 @@ class TestFileJpeg:
def test_photoshop_malformed_and_multiple(self) -> None: def test_photoshop_malformed_and_multiple(self) -> None:
with Image.open("Tests/images/app13-multiple.jpg") as im: with Image.open("Tests/images/app13-multiple.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert "photoshop" in im.info assert "photoshop" in im.info
assert 24 == len(im.info["photoshop"]) assert 24 == len(im.info["photoshop"])
apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"]
@ -1005,7 +1014,7 @@ class TestFileJpeg:
assert im.getxmp() == {"xmpmeta": None} assert im.getxmp() == {"xmpmeta": None}
def test_save_xmp(self, tmp_path: Path) -> None: def test_save_xmp(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg") f = tmp_path / "temp.jpg"
im = hopper() im = hopper()
im.save(f, xmp=b"XMP test") im.save(f, xmp=b"XMP test")
with Image.open(f) as reloaded: with Image.open(f) as reloaded:
@ -1024,7 +1033,7 @@ class TestFileJpeg:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(f, xmp=b"1" * 65505) im.save(f, xmp=b"1" * 65505)
@pytest.mark.timeout(timeout=1) @timeout_unless_slower_valgrind(1)
def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Even though this decoder never says that it is finished # Even though this decoder never says that it is finished
# the image should still end when there is no new data # the image should still end when there is no new data
@ -1055,10 +1064,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]
@ -1084,9 +1099,10 @@ class TestFileJpeg:
def test_deprecation(self) -> None: def test_deprecation(self) -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
with pytest.warns(DeprecationWarning): assert isinstance(im, JpegImagePlugin.JpegImageFile)
with pytest.warns(DeprecationWarning, match="huffman_ac"):
assert im.huffman_ac == {} assert im.huffman_ac == {}
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning, match="huffman_dc"):
assert im.huffman_dc == {} assert im.huffman_dc == {}
@ -1094,7 +1110,7 @@ class TestFileJpeg:
@skip_unless_feature("jpg") @skip_unless_feature("jpg")
class TestFileCloseW32: class TestFileCloseW32:
def test_fd_leak(self, tmp_path: Path) -> None: def test_fd_leak(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.jpg") tmpfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
im.save(tmpfile) im.save(tmpfile)

View File

@ -63,6 +63,7 @@ def test_sanity() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im: with Image.open("Tests/images/test-card-lossless.jp2") as im:
px = im.load() px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0) assert px[0, 0] == (0, 0, 0)
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (640, 480) assert im.size == (640, 480)
@ -98,7 +99,7 @@ def test_bytesio(card: ImageFile.ImageFile) -> None:
def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None: def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im: with Image.open("Tests/images/test-card-lossless.jp2") as im:
im.load() im.load()
outfile = str(tmp_path / "temp_test-card.png") outfile = tmp_path / "temp_test-card.png"
im.save(outfile) im.save(outfile)
assert_image_similar(im, card, 1.0e-3) assert_image_similar(im, card, 1.0e-3)
@ -212,7 +213,7 @@ def test_header_errors() -> None:
def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp_layers.jp2") outfile = tmp_path / "temp_layers.jp2"
for quality_layers in [[100, 50, 10], (100, 50, 10), None]: for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
card.save(outfile, quality_layers=quality_layers) card.save(outfile, quality_layers=quality_layers)
@ -227,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None:
out.seek(0) out.seek(0)
with Image.open(out) as im: with Image.open(out) as im:
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
im.layers = 1 im.layers = 1
im.load() im.load()
assert_image_similar(im, card, 13) assert_image_similar(im, card, 13)
out.seek(0) out.seek(0)
with Image.open(out) as im: with Image.open(out) as im:
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
im.layers = 3 im.layers = 3
im.load() im.load()
assert_image_similar(im, card, 0.4) assert_image_similar(im, card, 0.4)
@ -288,7 +291,7 @@ def test_mct(card: ImageFile.ImageFile) -> None:
def test_sgnd(tmp_path: Path) -> None: def test_sgnd(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.jp2") outfile = tmp_path / "temp.jp2"
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
im.save(outfile) im.save(outfile)
@ -312,6 +315,18 @@ def test_rgba(ext: str) -> None:
assert im.mode == "RGBA" assert im.mode == "RGBA"
def test_grayscale_four_channels() -> None:
with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp:
data = fp.read()
# Change color space to OPJ_CLRSPC_GRAY
data = data[:76] + b"\x11" + data[77:]
with Image.open(BytesIO(data)) as im:
im.load()
assert im.mode == "RGBA"
@pytest.mark.skipif( @pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
) )
@ -421,6 +436,7 @@ def test_subsampling_decode(name: str) -> None:
def test_pclr() -> None: def test_pclr() -> None:
with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im: with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im:
assert im.mode == "P" assert im.mode == "P"
assert im.palette is not None
assert len(im.palette.colors) == 256 assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0 assert im.palette.colors[(255, 255, 255)] == 0
@ -428,6 +444,7 @@ def test_pclr() -> None:
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im: ) as im:
assert im.mode == "P" assert im.mode == "P"
assert im.palette is not None
assert len(im.palette.colors) == 139 assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0 assert im.palette.colors[(0, 0, 0, 0)] == 0

View File

@ -36,10 +36,11 @@ class LibTiffTestCase:
im.load() im.load()
im.getdata() im.getdata()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im._compression == "group4" assert im._compression == "group4"
# can we write it back out, in a different form. # can we write it back out, in a different form.
out = str(tmp_path / "temp.png") out = tmp_path / "temp.png"
im.save(out) im.save(out)
out_bytes = io.BytesIO() out_bytes = io.BytesIO()
@ -123,7 +124,7 @@ class TestFileLibTiff(LibTiffTestCase):
"""Checking to see that the saved image is the same as what we wrote""" """Checking to see that the saved image is the same as what we wrote"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig: with Image.open(test_file) as orig:
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
rot = orig.transpose(Image.Transpose.ROTATE_90) rot = orig.transpose(Image.Transpose.ROTATE_90)
assert rot.size == (500, 500) assert rot.size == (500, 500)
rot.save(out) rot.save(out)
@ -151,8 +152,9 @@ class TestFileLibTiff(LibTiffTestCase):
@pytest.mark.parametrize("legacy_api", (False, True)) @pytest.mark.parametrize("legacy_api", (False, True))
def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None:
"""Test metadata writing through libtiff""" """Test metadata writing through libtiff"""
f = str(tmp_path / "temp.tiff") f = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper_g4.tif") as img: with Image.open("Tests/images/hopper_g4.tif") as img:
assert isinstance(img, TiffImagePlugin.TiffImageFile)
img.save(f, tiffinfo=img.tag) img.save(f, tiffinfo=img.tag)
if legacy_api: if legacy_api:
@ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase):
] ]
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
if legacy_api: if legacy_api:
reloaded = loaded.tag.named() reloaded = loaded.tag.named()
else: else:
@ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Exclude ones that have special meaning # Exclude ones that have special meaning
# that we're already testing them # that we're already testing them
with Image.open("Tests/images/hopper_g4.tif") as im: with Image.open("Tests/images/hopper_g4.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
for tag in im.tag_v2: for tag in im.tag_v2:
try: try:
del core_items[tag] del core_items[tag]
@ -247,7 +251,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Extra samples really doesn't make sense in this application. # Extra samples really doesn't make sense in this application.
del new_ifd[338] del new_ifd[338]
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, tiffinfo=new_ifd) im.save(out, tiffinfo=new_ifd)
@ -313,10 +317,11 @@ class TestFileLibTiff(LibTiffTestCase):
) -> None: ) -> None:
im = hopper() im = hopper()
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
im.save(out, tiffinfo=tiffinfo) im.save(out, tiffinfo=tiffinfo)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
for tag, value in tiffinfo.items(): for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag] reloaded_value = reloaded.tag_v2[tag]
if ( if (
@ -347,14 +352,16 @@ class TestFileLibTiff(LibTiffTestCase):
) )
def test_osubfiletype(self, tmp_path: Path) -> None: def test_osubfiletype(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/g4_orientation_6.tif") as im: with Image.open("Tests/images/g4_orientation_6.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[OSUBFILETYPE] = 1 im.tag_v2[OSUBFILETYPE] = 1
im.save(outfile) im.save(outfile)
def test_subifd(self, tmp_path: Path) -> None: def test_subifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/g4_orientation_6.tif") as im: with Image.open("Tests/images/g4_orientation_6.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[SUBIFD] = 10000 im.tag_v2[SUBIFD] = 10000
# Should not segfault # Should not segfault
@ -365,17 +372,18 @@ class TestFileLibTiff(LibTiffTestCase):
) -> None: ) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
if 700 in reloaded.tag_v2: 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
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, dpi=(72, 72)) im.save(out, dpi=(72, 72))
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -383,7 +391,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g3_compression(self, tmp_path: Path) -> None: def test_g3_compression(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper_g4_500.tif") as i: with Image.open("Tests/images/hopper_g4_500.tif") as i:
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
i.save(out, compression="group3") i.save(out, compression="group3")
with Image.open(out) as reread: with Image.open(out) as reread:
@ -400,7 +408,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert b[0] == ord(b"\xe0") assert b[0] == ord(b"\xe0")
assert b[1] == ord(b"\x01") assert b[1] == ord(b"\x01")
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
# out = "temp.le.tif" # out = "temp.le.tif"
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
@ -420,7 +428,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert b[0] == ord(b"\x01") assert b[0] == ord(b"\x01")
assert b[1] == ord(b"\xe0") assert b[1] == ord(b"\xe0")
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["compression"] == im.info["compression"] assert reread.info["compression"] == im.info["compression"]
@ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase):
"""Tests String data in info directory""" """Tests String data in info directory"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig: with Image.open(test_file) as orig:
out = str(tmp_path / "temp.tif") assert isinstance(orig, TiffImagePlugin.TiffImageFile)
out = tmp_path / "temp.tif"
orig.tag[269] = "temp.tif" orig.tag[269] = "temp.tif"
orig.save(out) orig.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag_v2[269]
assert "temp.tif" == reread.tag[269][0] assert "temp.tif" == reread.tag[269][0]
@ -457,7 +468,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_blur(self, tmp_path: Path) -> None: def test_blur(self, tmp_path: Path) -> None:
# test case from irc, how to do blur on b/w image # test case from irc, how to do blur on b/w image
# and save to compressed tif. # and save to compressed tif.
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
with Image.open("Tests/images/pport_g4.tif") as im: with Image.open("Tests/images/pport_g4.tif") as im:
im = im.convert("L") im = im.convert("L")
@ -470,7 +481,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Test various tiff compressions and assert similar image content but reduced # Test various tiff compressions and assert similar image content but reduced
# file sizes. # file sizes.
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
im.save(out) im.save(out)
size_raw = os.path.getsize(out) size_raw = os.path.getsize(out)
@ -494,7 +505,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: def test_tiff_jpeg_compression(self, tmp_path: Path) -> None:
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
im.save(out, compression="tiff_jpeg") im.save(out, compression="tiff_jpeg")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -502,7 +513,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiff_deflate_compression(self, tmp_path: Path) -> None: def test_tiff_deflate_compression(self, tmp_path: Path) -> None:
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
im.save(out, compression="tiff_deflate") im.save(out, compression="tiff_deflate")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -510,7 +521,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_quality(self, tmp_path: Path) -> None: def test_quality(self, tmp_path: Path) -> None:
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(out, compression="tiff_lzw", quality=50) im.save(out, compression="tiff_lzw", quality=50)
@ -525,7 +536,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_cmyk_save(self, tmp_path: Path) -> None: def test_cmyk_save(self, tmp_path: Path) -> None:
im = hopper("CMYK") im = hopper("CMYK")
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
im.save(out, compression="tiff_adobe_deflate") im.save(out, compression="tiff_adobe_deflate")
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
@ -534,19 +545,20 @@ class TestFileLibTiff(LibTiffTestCase):
def test_palette_save( def test_palette_save(
self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None: ) -> None:
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
# colormap/palette tag # colormap/palette tag
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert len(reloaded.tag_v2[320]) == 768 assert len(reloaded.tag_v2[320]) == 768
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(out, compression=compression) im.save(out, compression=compression)
@ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.seek(0) im.seek(0)
assert im.size == (10, 10) assert im.size == (10, 10)
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
@ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase):
# issue #862 # issue #862
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
frames = im.n_frames frames = im.n_frames
assert frames == 3 assert frames == 3
for _ in range(frames): for _ in range(frames):
@ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert not im.tag.next assert not im.tag.next
im.load() im.load()
assert not im.tag.next assert not im.tag.next
@ -686,25 +701,29 @@ class TestFileLibTiff(LibTiffTestCase):
def test_save_ycbcr(self, tmp_path: Path) -> None: def test_save_ycbcr(self, tmp_path: Path) -> None:
im = hopper("YCbCr") im = hopper("YCbCr")
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
im.save(outfile, compression="jpeg") im.save(outfile, compression="jpeg")
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
def test_exif_ifd(self) -> None: def test_exif_ifd(self) -> None:
out = io.BytesIO() out = io.BytesIO()
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[34665] == 125456 assert im.tag_v2[34665] == 125456
im.save(out, "TIFF") im.save(out, "TIFF")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert 34665 not in reloaded.tag_v2 assert 34665 not in reloaded.tag_v2
im.save(out, "TIFF", tiffinfo={34665: 125456}) im.save(out, "TIFF", tiffinfo={34665: 125456})
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
if Image.core.libtiff_support_custom_tags: if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456 assert reloaded.tag_v2[34665] == 125456
@ -713,7 +732,7 @@ class TestFileLibTiff(LibTiffTestCase):
) -> None: ) -> None:
# issue 1597 # issue 1597
with Image.open("Tests/images/rdf.tif") as im: with Image.open("Tests/images/rdf.tif") as im:
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
# this shouldn't crash # this shouldn't crash
@ -724,7 +743,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Test TIFF with tag 297 (Page Number) having value of 0 0. # Test TIFF with tag 297 (Page Number) having value of 0 0.
# The first number is the current page number. # The first number is the current page number.
# The second is the total number of pages, zero means not available. # The second is the total number of pages, zero means not available.
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
# Created by printing a page in Chrome to PDF, then: # Created by printing a page in Chrome to PDF, then:
# /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif
# -dNOPAUSE /tmp/test.pdf -c quit # -dNOPAUSE /tmp/test.pdf -c quit
@ -736,7 +755,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_fd_duplication(self, tmp_path: Path) -> None: def test_fd_duplication(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/1651 # https://github.com/python-pillow/Pillow/issues/1651
tmpfile = str(tmp_path / "temp.tif") tmpfile = tmp_path / "temp.tif"
with open(tmpfile, "wb") as f: with open(tmpfile, "wb") as f:
with open("Tests/images/g4-multi.tiff", "rb") as src: with open("Tests/images/g4-multi.tiff", "rb") as src:
f.write(src.read()) f.write(src.read())
@ -779,13 +798,14 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/hopper.iccprofile.tif") as img: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_profile = img.info["icc_profile"] icc_profile = img.info["icc_profile"]
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
img.save(out, icc_profile=icc_profile) img.save(out, icc_profile=icc_profile)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert icc_profile == reloaded.info["icc_profile"] assert icc_profile == reloaded.info["icc_profile"]
def test_multipage_compression(self) -> None: def test_multipage_compression(self) -> None:
with Image.open("Tests/images/compression.tif") as im: with Image.open("Tests/images/compression.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.seek(0) im.seek(0)
assert im._compression == "tiff_ccitt" assert im._compression == "tiff_ccitt"
assert im.size == (10, 10) assert im.size == (10, 10)
@ -802,7 +822,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None:
# Arrange # Arrange
outfile = str(tmp_path / "temp.tif") outfile = tmp_path / "temp.tif"
# Created with ImageMagick: convert hopper.jpg hopper_jpg.tif # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif
# Contains JPEGTables (347) tag # Contains JPEGTables (347) tag
@ -864,7 +884,7 @@ class TestFileLibTiff(LibTiffTestCase):
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None: ) -> None:
im = Image.new("F", (1, 1)) im = Image.new("F", (1, 1))
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out) im.save(out)
@ -1008,7 +1028,7 @@ class TestFileLibTiff(LibTiffTestCase):
@pytest.mark.parametrize("compression", (None, "jpeg")) @pytest.mark.parametrize("compression", (None, "jpeg"))
def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None:
im = hopper() im = hopper()
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
tags = { tags = {
TiffImagePlugin.TILEWIDTH: 256, TiffImagePlugin.TILEWIDTH: 256,
@ -1026,6 +1046,17 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: with Image.open("Tests/images/old-style-jpeg-compression.tif") as im:
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
def test_old_style_jpeg_orientation(self) -> None:
with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp:
data = fp.read()
# Set EXIF Orientation to 2
data = data[:102] + b"\x02" + data[103:]
with Image.open(io.BytesIO(data)) as im:
im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
def test_open_missing_samplesperpixel(self) -> None: def test_open_missing_samplesperpixel(self) -> None:
with Image.open( with Image.open(
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif"
@ -1079,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im: with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9): for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 274 in im.tag_v2 assert 274 in im.tag_v2
im.load() im.load()
@ -1140,16 +1172,14 @@ class TestFileLibTiff(LibTiffTestCase):
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e:
im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY # Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "decoder error -9" with pytest.raises(OSError, match="decoder error -9"):
im.load()
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256)) im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
im.save(out, compression=compression) im.save(out, compression=compression)
with Image.open(out) as im: with Image.open(out) as im:
@ -1162,7 +1192,7 @@ class TestFileLibTiff(LibTiffTestCase):
self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
im = hopper("RGB").resize((256, 256)) im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
if not argument: if not argument:
monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18) monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18)
@ -1178,13 +1208,13 @@ class TestFileLibTiff(LibTiffTestCase):
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: def test_save_zero(self, compression: str | None, tmp_path: Path) -> None:
im = Image.new("RGB", (0, 0)) im = Image.new("RGB", (0, 0))
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
with pytest.raises(SystemError): with pytest.raises(SystemError):
im.save(out, compression=compression) im.save(out, compression=compression)
def test_save_many_compressed(self, tmp_path: Path) -> None: def test_save_many_compressed(self, tmp_path: Path) -> None:
im = hopper() im = hopper()
out = str(tmp_path / "temp.tif") out = tmp_path / "temp.tif"
for _ in range(10000): for _ in range(10000):
im.save(out, compression="jpeg") im.save(out, compression="jpeg")

View File

@ -30,11 +30,13 @@ def test_sanity() -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert isinstance(im, MicImagePlugin.MicImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
def test_is_animated() -> None: def test_is_animated() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert isinstance(im, MicImagePlugin.MicImageFile)
assert not im.is_animated assert not im.is_animated
@ -55,10 +57,11 @@ def test_seek() -> None:
def test_close() -> None: def test_close() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
pass assert isinstance(im, MicImagePlugin.MicImageFile)
assert im.ole.fp.closed assert im.ole.fp.closed
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
assert isinstance(im, MicImagePlugin.MicImageFile)
im.close() im.close()
assert im.ole.fp.closed assert im.ole.fp.closed

View File

@ -6,7 +6,7 @@ from typing import Any
import pytest import pytest
from PIL import Image, ImageFile, MpoImagePlugin from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -29,21 +29,26 @@ def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_sanity(test_file: str) -> None: def test_sanity(test_file: str) -> None:
with Image.open(test_file) as im: def check(im: ImageFile.ImageFile) -> None:
im.load() im.load()
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (640, 480) assert im.size == (640, 480)
assert im.format == "MPO" assert im.format == "MPO"
with Image.open(test_file) as im:
check(im)
with MpoImagePlugin.MpoImageFile(test_file) as im:
check(im)
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None: def test_unclosed_file() -> None:
def open() -> None: def open_test_image() -> None:
im = Image.open(test_files[0]) im = Image.open(test_files[0])
im.load() im.load()
with pytest.warns(ResourceWarning): with pytest.warns(ResourceWarning):
open() open_test_image()
def test_closed_file() -> None: def test_closed_file() -> None:
@ -75,10 +80,11 @@ def test_context_manager() -> None:
def test_app(test_file: str) -> None: def test_app(test_file: str) -> None:
# Test APP/COM reader (@PIL135) # Test APP/COM reader (@PIL135)
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.applist[0][0] == "APP1" assert im.applist[0][0] == "APP1"
assert im.applist[1][0] == "APP2" assert im.applist[1][0] == "APP2"
assert ( assert im.applist[1][1].startswith(
im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
) )
assert len(im.applist) == 2 assert len(im.applist) == 2
@ -150,6 +156,7 @@ def test_reload_exif_after_seek() -> None:
def test_mp(test_file: str) -> None: def test_mp(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo is not None
assert mpinfo[45056] == b"0100" assert mpinfo[45056] == b"0100"
assert mpinfo[45057] == 2 assert mpinfo[45057] == 2
@ -159,6 +166,7 @@ def test_mp_offset() -> None:
# in APP2 data, in contrast to normal 8 # in APP2 data, in contrast to normal 8
with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo is not None
assert mpinfo[45056] == b"0100" assert mpinfo[45056] == b"0100"
assert mpinfo[45057] == 2 assert mpinfo[45057] == 2
@ -175,6 +183,7 @@ def test_mp_no_data() -> None:
def test_mp_attribute(test_file: str) -> None: def test_mp_attribute(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo is not None
for frame_number, mpentry in enumerate(mpinfo[0xB002]): for frame_number, mpentry in enumerate(mpinfo[0xB002]):
mpattr = mpentry["Attribute"] mpattr = mpentry["Attribute"]
if frame_number: if frame_number:
@ -215,12 +224,14 @@ def test_seek(test_file: str) -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im: with Image.open("Tests/images/sugarshack.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.n_frames == 2 assert im.n_frames == 2
assert im.is_animated assert im.is_animated
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im: with Image.open("Tests/images/sugarshack.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
n_frames = im.n_frames n_frames = im.n_frames
# Test seeking past the last frame # Test seeking past the last frame
@ -234,6 +245,8 @@ def test_eoferror() -> None:
def test_adopt_jpeg() -> None: def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
with pytest.raises(ValueError): with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im) MpoImagePlugin.MpoImageFile.adopt(im)
@ -305,6 +318,9 @@ def test_save_xmp() -> None:
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]) im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2])
# Test that encoderinfo is unchanged
assert im2.encoderinfo == {"xmp": b"Second frame"}
assert im_reloaded.info["xmp"] == b"First frame" assert im_reloaded.info["xmp"] == b"First frame"
im_reloaded.seek(1) im_reloaded.seek(1)

View File

@ -15,7 +15,7 @@ YA_EXTRA_DIR = "Tests/images/msp"
def test_sanity(tmp_path: Path) -> None: def test_sanity(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.msp") test_file = tmp_path / "temp.msp"
hopper("1").save(test_file) hopper("1").save(test_file)
@ -84,7 +84,7 @@ def test_msp_v2() -> None:
def test_cannot_save_wrong_mode(tmp_path: Path) -> None: def test_cannot_save_wrong_mode(tmp_path: Path) -> None:
# Arrange # Arrange
im = hopper() im = hopper()
filename = str(tmp_path / "temp.msp") filename = tmp_path / "temp.msp"
# Act/Assert # Act/Assert
with pytest.raises(OSError): with pytest.raises(OSError):

View File

@ -14,7 +14,7 @@ from .helper import assert_image_equal, hopper, magick_command
def helper_save_as_palm(tmp_path: Path, mode: str) -> None: def helper_save_as_palm(tmp_path: Path, mode: str) -> None:
# Arrange # Arrange
im = hopper(mode) im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".palm")) outfile = tmp_path / ("temp_" + mode + ".palm")
# Act # Act
im.save(outfile) im.save(outfile)
@ -25,7 +25,7 @@ def helper_save_as_palm(tmp_path: Path, mode: str) -> None:
def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image:
outfile = str(tmp_path / "temp.png") outfile = tmp_path / "temp.png"
rc = subprocess.call( rc = subprocess.call(
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
) )
@ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None:
im.save(outfile) im.save(outfile)
converted = open_with_magick(magick, tmp_path, outfile) converted = open_with_magick(magick, tmp_path, outfile)
if mode == "P":
assert converted.mode == "P"
im = im.convert("RGB")
converted = converted.convert("RGB")
assert_image_equal(converted, im) assert_image_equal(converted, im)
@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None:
roundtrip(tmp_path, mode) roundtrip(tmp_path, mode)
@pytest.mark.xfail(reason="Palm P image is wrong")
def test_p_mode(tmp_path: Path) -> None: def test_p_mode(tmp_path: Path) -> None:
# Arrange # Arrange
mode = "P" mode = "P"

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import io
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper
def _roundtrip(tmp_path: Path, im: Image.Image) -> None: def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
f = str(tmp_path / "temp.pcx") f = tmp_path / "temp.pcx"
im.save(f) im.save(f)
with Image.open(f) as im2: with Image.open(f) as im2:
assert im2.mode == im.mode assert im2.mode == im.mode
@ -30,12 +31,39 @@ def test_sanity(tmp_path: Path) -> None:
_roundtrip(tmp_path, im) _roundtrip(tmp_path, im)
# Test an unsupported mode # Test an unsupported mode
f = str(tmp_path / "temp.pcx") f = tmp_path / "temp.pcx"
im = hopper("RGBA") im = hopper("RGBA")
with pytest.raises(ValueError): with pytest.raises(ValueError):
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:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()
data = data[:4] + b"\xff\xff" + data[6:]
b = io.BytesIO(data)
with pytest.raises(SyntaxError, match="bad PCX image size"):
with PcxImagePlugin.PcxImageFile(b):
pass
def test_unknown_mode() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()
data = data[:3] + b"\xff" + data[4:]
b = io.BytesIO(data)
with pytest.raises(OSError, match="unknown PCX mode"):
with Image.open(b):
pass
def test_invalid_file() -> None: def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"

View File

@ -13,7 +13,12 @@ import pytest
from PIL import Image, PdfParser, features from PIL import Image, PdfParser, features
from .helper import hopper, mark_if_feature_version, skip_unless_feature from .helper import (
hopper,
mark_if_feature_version,
skip_unless_feature,
timeout_unless_slower_valgrind,
)
def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
@ -55,7 +60,7 @@ def test_save_alpha(tmp_path: Path, mode: str) -> None:
def test_p_alpha(tmp_path: Path) -> None: def test_p_alpha(tmp_path: Path) -> None:
# Arrange # Arrange
outfile = str(tmp_path / "temp.pdf") outfile = tmp_path / "temp.pdf"
with Image.open("Tests/images/pil123p.png") as im: with Image.open("Tests/images/pil123p.png") as im:
assert im.mode == "P" assert im.mode == "P"
assert isinstance(im.info["transparency"], bytes) assert isinstance(im.info["transparency"], bytes)
@ -80,7 +85,7 @@ def test_monochrome(tmp_path: Path) -> None:
def test_unsupported_mode(tmp_path: Path) -> None: def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("PA") im = hopper("PA")
outfile = str(tmp_path / "temp_PA.pdf") outfile = tmp_path / "temp_PA.pdf"
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(outfile) im.save(outfile)
@ -89,7 +94,7 @@ def test_unsupported_mode(tmp_path: Path) -> None:
def test_resolution(tmp_path: Path) -> None: def test_resolution(tmp_path: Path) -> None:
im = hopper() im = hopper()
outfile = str(tmp_path / "temp.pdf") outfile = tmp_path / "temp.pdf"
im.save(outfile, resolution=150) im.save(outfile, resolution=150)
with open(outfile, "rb") as fp: with open(outfile, "rb") as fp:
@ -117,7 +122,7 @@ def test_resolution(tmp_path: Path) -> None:
def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper() im = hopper()
outfile = str(tmp_path / "temp.pdf") outfile = tmp_path / "temp.pdf"
im.save(outfile, "PDF", **params) im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp: with open(outfile, "rb") as fp:
@ -144,7 +149,7 @@ def test_save_all(tmp_path: Path) -> None:
# Multiframe image # Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
outfile = str(tmp_path / "temp.pdf") outfile = tmp_path / "temp.pdf"
im.save(outfile, save_all=True) im.save(outfile, save_all=True)
assert os.path.isfile(outfile) assert os.path.isfile(outfile)
@ -177,7 +182,7 @@ def test_save_all(tmp_path: Path) -> None:
def test_multiframe_normal_save(tmp_path: Path) -> None: def test_multiframe_normal_save(tmp_path: Path) -> None:
# Test saving a multiframe image without save_all # Test saving a multiframe image without save_all
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
outfile = str(tmp_path / "temp.pdf") outfile = tmp_path / "temp.pdf"
im.save(outfile) im.save(outfile)
assert os.path.isfile(outfile) assert os.path.isfile(outfile)
@ -339,8 +344,7 @@ def test_pdf_append_to_bytesio() -> None:
assert len(f.getvalue()) > initial_size assert len(f.getvalue()) > initial_size
@pytest.mark.timeout(1) @timeout_unless_slower_valgrind(1)
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
@pytest.mark.parametrize("newline", (b"\r", b"\n")) @pytest.mark.parametrize("newline", (b"\r", b"\n"))
def test_redos(newline: bytes) -> None: def test_redos(newline: bytes) -> None:
malicious = b" trailer<<>>" + newline * 3456 malicious = b" trailer<<>>" + newline * 3456

View File

@ -68,7 +68,7 @@ def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile:
@skip_unless_feature("zlib") @skip_unless_feature("zlib")
class TestFilePng: class TestFilePng:
def get_chunks(self, filename: str) -> list[bytes]: def get_chunks(self, filename: Path) -> list[bytes]:
chunks = [] chunks = []
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
fp.read(8) fp.read(8)
@ -89,7 +89,7 @@ class TestFilePng:
assert version is not None assert version is not None
assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version)
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
hopper("RGB").save(test_file) hopper("RGB").save(test_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)
@ -250,7 +250,7 @@ class TestFilePng:
# each palette entry # each palette entry
assert len(im.info["transparency"]) == 256 assert len(im.info["transparency"]) == 256
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
# check if saved image contains same transparency # check if saved image contains same transparency
@ -271,7 +271,7 @@ class TestFilePng:
assert im.info["transparency"] == 164 assert im.info["transparency"] == 164
assert im.getpixel((31, 31)) == 164 assert im.getpixel((31, 31)) == 164
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
# check if saved image contains same transparency # check if saved image contains same transparency
@ -294,7 +294,7 @@ class TestFilePng:
assert im.getcolors() == [(100, (0, 0, 0, 0))] assert im.getcolors() == [(100, (0, 0, 0, 0))]
im = im.convert("P") im = im.convert("P")
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
# check if saved image contains same transparency # check if saved image contains same transparency
@ -315,7 +315,7 @@ class TestFilePng:
im_rgba = im.convert("RGBA") im_rgba = im.convert("RGBA")
assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
with Image.open(test_file) as test_im: with Image.open(test_file) as test_im:
@ -329,7 +329,7 @@ class TestFilePng:
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"
with Image.open(in_file) as im: with Image.open(in_file) as im:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file) im.save(test_file)
def test_load_verify(self) -> None: def test_load_verify(self) -> None:
@ -488,7 +488,7 @@ class TestFilePng:
im = hopper("P") im = hopper("P")
im.info["transparency"] = 0 im.info["transparency"] = 0
f = str(tmp_path / "temp.png") f = tmp_path / "temp.png"
im.save(f) im.save(f)
with Image.open(f) as im2: with Image.open(f) as im2:
@ -549,7 +549,7 @@ class TestFilePng:
def test_chunk_order(self, tmp_path: Path) -> None: def test_chunk_order(self, tmp_path: Path) -> None:
with Image.open("Tests/images/icc_profile.png") as im: with Image.open("Tests/images/icc_profile.png") as im:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.convert("P").save(test_file, dpi=(100, 100)) im.convert("P").save(test_file, dpi=(100, 100))
chunks = self.get_chunks(test_file) chunks = self.get_chunks(test_file)
@ -576,6 +576,7 @@ class TestFilePng:
def test_read_private_chunks(self) -> None: def test_read_private_chunks(self) -> None:
with Image.open("Tests/images/exif.png") as im: with Image.open("Tests/images/exif.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.private_chunks == [(b"orNT", b"\x01")] assert im.private_chunks == [(b"orNT", b"\x01")]
def test_roundtrip_private_chunk(self) -> None: def test_roundtrip_private_chunk(self) -> None:
@ -598,6 +599,7 @@ class TestFilePng:
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/hopper.png") as im: with Image.open("Tests/images/hopper.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert "comment" in im.text assert "comment" in im.text
for k, v in { for k, v in {
"date:create": "2014-09-04T09:37:08+03:00", "date:create": "2014-09-04T09:37:08+03:00",
@ -607,15 +609,19 @@ class TestFilePng:
# Raises a SyntaxError in load_end # Raises a SyntaxError in load_end
with Image.open("Tests/images/broken_data_stream.png") as im: with Image.open("Tests/images/broken_data_stream.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(OSError): with pytest.raises(OSError):
assert isinstance(im.text, dict) assert isinstance(im.text, dict)
# Raises an EOFError in load_end # Raises an EOFError in load_end
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
# Raises a UnicodeDecodeError in load_end # Raises a UnicodeDecodeError in load_end
with Image.open("Tests/images/truncated_image.png") as im: with Image.open("Tests/images/truncated_image.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
# The file is truncated # The file is truncated
with pytest.raises(OSError): with pytest.raises(OSError):
im.text im.text
@ -661,7 +667,7 @@ class TestFilePng:
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
im = hopper("P") im = hopper("P")
out = str(tmp_path / "temp.png") out = tmp_path / "temp.png"
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:
@ -671,8 +677,8 @@ class TestFilePng:
im = Image.new("P", (1, 1)) im = Image.new("P", (1, 1))
im.putpalette((1, 1, 1)) im.putpalette((1, 1, 1))
out = str(tmp_path / "temp.png") out = tmp_path / "temp.png"
im.save(str(tmp_path / "temp.png")) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 3 assert len(reloaded.png.im_palette[1]) == 3
@ -721,11 +727,12 @@ class TestFilePng:
def test_exif_save(self, tmp_path: Path) -> None: def test_exif_save(self, tmp_path: Path) -> None:
# Test exif is not saved from info # Test exif is not saved from info
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
with Image.open("Tests/images/exif.png") as im: with Image.open("Tests/images/exif.png") as im:
im.save(test_file) im.save(test_file)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
assert reloaded._getexif() is None assert reloaded._getexif() is None
# Test passing in exif # Test passing in exif
@ -741,7 +748,7 @@ class TestFilePng:
) )
def test_exif_from_jpg(self, tmp_path: Path) -> None: def test_exif_from_jpg(self, tmp_path: Path) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im: with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
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:
@ -750,7 +757,7 @@ class TestFilePng:
def test_exif_argument(self, tmp_path: Path) -> None: def test_exif_argument(self, tmp_path: Path) -> None:
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
test_file = str(tmp_path / "temp.png") test_file = tmp_path / "temp.png"
im.save(test_file, exif=b"exifstring") im.save(test_file, exif=b"exifstring")
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
@ -794,6 +801,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

@ -79,6 +79,7 @@ def test_arbitrary_maxval(
assert im.mode == mode assert im.mode == mode
px = im.load() px = im.load()
assert px is not None
assert tuple(px[x, 0] for x in range(3)) == pixels assert tuple(px[x, 0] for x in range(3)) == pixels
@ -93,7 +94,7 @@ def test_16bit_pgm() -> None:
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 = str(tmp_path / "temp.pgm") filename = tmp_path / "temp.pgm"
im.save(filename, "PPM") im.save(filename, "PPM")
assert_image_equal_tofile(im, filename) assert_image_equal_tofile(im, filename)
@ -105,7 +106,7 @@ def test_pnm(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.pnm") as im: with Image.open("Tests/images/hopper.pnm") as im:
assert_image_similar(im, hopper(), 0.0001) assert_image_similar(im, hopper(), 0.0001)
filename = str(tmp_path / "temp.pnm") filename = tmp_path / "temp.pnm"
im.save(filename) im.save(filename)
assert_image_equal_tofile(im, filename) assert_image_equal_tofile(im, filename)
@ -116,7 +117,7 @@ def test_pfm(tmp_path: Path) -> None:
assert im.info["scale"] == 1.0 assert im.info["scale"] == 1.0
assert_image_equal(im, hopper("F")) assert_image_equal(im, hopper("F"))
filename = str(tmp_path / "tmp.pfm") filename = tmp_path / "tmp.pfm"
im.save(filename) im.save(filename)
assert_image_equal_tofile(im, filename) assert_image_equal_tofile(im, filename)
@ -127,7 +128,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None:
assert im.info["scale"] == 2.5 assert im.info["scale"] == 2.5
assert_image_equal(im, hopper("F")) assert_image_equal(im, hopper("F"))
filename = str(tmp_path / "tmp.pfm") filename = tmp_path / "tmp.pfm"
im.save(filename) im.save(filename)
assert_image_equal_tofile(im, filename) assert_image_equal_tofile(im, filename)
@ -193,8 +194,8 @@ def test_16bit_plain_pgm() -> None:
def test_plain_data_with_comment( def test_plain_data_with_comment(
tmp_path: Path, header: bytes, data: bytes, comment_count: int tmp_path: Path, header: bytes, data: bytes, comment_count: int
) -> None: ) -> None:
path1 = str(tmp_path / "temp1.ppm") path1 = tmp_path / "temp1.ppm"
path2 = str(tmp_path / "temp2.ppm") path2 = tmp_path / "temp2.ppm"
comment = b"# comment" * comment_count comment = b"# comment" * comment_count
with open(path1, "wb") as f1, open(path2, "wb") as f2: with open(path1, "wb") as f1, open(path2, "wb") as f2:
f1.write(header + b"\n\n" + data) f1.write(header + b"\n\n" + data)
@ -206,7 +207,7 @@ def test_plain_data_with_comment(
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:
path = str(tmp_path / "temp.ppm") path = tmp_path / "temp.ppm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(data) f.write(data)
@ -217,7 +218,7 @@ def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:
path = str(tmp_path / "temp.ppm") path = tmp_path / "temp.ppm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(data) f.write(data)
@ -234,7 +235,7 @@ def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:
), ),
) )
def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
path = str(tmp_path / "temp.ppm") path = tmp_path / "temp.ppm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(data) f.write(data)
@ -244,7 +245,7 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
def test_plain_ppm_value_negative(tmp_path: Path) -> None: def test_plain_ppm_value_negative(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm") path = tmp_path / "temp.ppm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n-1") f.write(b"P3\n128 128\n255\n-1")
@ -254,7 +255,7 @@ def test_plain_ppm_value_negative(tmp_path: Path) -> None:
def test_plain_ppm_value_too_large(tmp_path: Path) -> None: def test_plain_ppm_value_too_large(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm") path = tmp_path / "temp.ppm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n256") f.write(b"P3\n128 128\n255\n256")
@ -269,7 +270,7 @@ def test_magic() -> None:
def test_header_with_comments(tmp_path: Path) -> None: def test_header_with_comments(tmp_path: Path) -> None:
path = str(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 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n")
@ -278,7 +279,7 @@ def test_header_with_comments(tmp_path: Path) -> None:
def test_non_integer_token(tmp_path: Path) -> None: def test_non_integer_token(tmp_path: Path) -> None:
path = str(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\nTEST") f.write(b"P6\nTEST")
@ -287,30 +288,28 @@ 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"))
path = str(tmp_path / "temp.ppm") def test_header_token_too_long(tmp_path: Path, data: bytes) -> None:
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) as e: 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)
assert str(e.value) == "Token too long in file header: 01234567890"
def test_truncated_file(tmp_path: Path) -> None: def test_truncated_file(tmp_path: Path) -> None:
# Test EOF in header # Test EOF in header
path = str(tmp_path / "temp.pgm") path = tmp_path / "temp.pgm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P6") f.write(b"P6")
with pytest.raises(ValueError) as e: with pytest.raises(ValueError, match="Reached EOF while reading header"):
with Image.open(path): with Image.open(path):
pass pass
assert str(e.value) == "Reached EOF while reading header"
# Test EOF for PyDecoder # Test EOF for PyDecoder
fp = BytesIO(b"P5 3 1 4") fp = BytesIO(b"P5 3 1 4")
with Image.open(fp) as im: with Image.open(fp) as im:
@ -319,7 +318,7 @@ def test_truncated_file(tmp_path: Path) -> None:
def test_not_enough_image_data(tmp_path: Path) -> None: def test_not_enough_image_data(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm") path = tmp_path / "temp.ppm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P2 1 2 255 255") f.write(b"P2 1 2 255 255")
@ -330,16 +329,16 @@ def test_not_enough_image_data(tmp_path: Path) -> None:
@pytest.mark.parametrize("maxval", (b"0", b"65536")) @pytest.mark.parametrize("maxval", (b"0", b"65536"))
def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:
path = str(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\n3 1 " + maxval) f.write(b"P6\n3 1 " + maxval)
with pytest.raises(ValueError) as e: with pytest.raises(
ValueError, match="maxval must be greater than 0 and less than 65536"
):
with Image.open(path): with Image.open(path):
pass pass
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
def test_neg_ppm() -> None: def test_neg_ppm() -> None:
# Storage.c accepted negative values for xsize, ysize. the # Storage.c accepted negative values for xsize, ysize. the
@ -353,7 +352,7 @@ def test_neg_ppm() -> None:
def test_mimetypes(tmp_path: Path) -> None: def test_mimetypes(tmp_path: Path) -> None:
path = str(tmp_path / "temp.pgm") path = tmp_path / "temp.pgm"
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P4\n128 128\n255") f.write(b"P4\n128 128\n255")

View File

@ -25,12 +25,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None: def test_unclosed_file() -> None:
def open() -> None: def open_test_image() -> None:
im = Image.open(test_file) im = Image.open(test_file)
im.load() im.load()
with pytest.warns(ResourceWarning): with pytest.warns(ResourceWarning):
open() open_test_image()
def test_closed_file() -> None: def test_closed_file() -> None:
@ -59,17 +59,21 @@ def test_invalid_file() -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open("Tests/images/hopper_merged.psd") as im: with Image.open("Tests/images/hopper_merged.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
for path in [test_file, "Tests/images/negative_layer_count.psd"]: for path in [test_file, "Tests/images/negative_layer_count.psd"]:
with Image.open(path) as im: with Image.open(path) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.n_frames == 2 assert im.n_frames == 2
assert im.is_animated assert im.is_animated
def test_eoferror() -> None: def test_eoferror() -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
# PSD seek index starts at 1 rather than 0 # PSD seek index starts at 1 rather than 0
n_frames = im.n_frames + 1 n_frames = im.n_frames + 1
@ -119,11 +123,13 @@ def test_rgba() -> None:
def test_negative_top_left_layer() -> None: def test_negative_top_left_layer() -> None:
with Image.open("Tests/images/negative_top_left_layer.psd") as im: with Image.open("Tests/images/negative_top_left_layer.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.layers[0][2] == (-50, -50, 50, 50) assert im.layers[0][2] == (-50, -50, 50, 50)
def test_layer_skip() -> None: def test_layer_skip() -> None:
with Image.open("Tests/images/five_channels.psd") as im: with Image.open("Tests/images/five_channels.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
@ -175,5 +181,6 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None:
def test_layer_crashes(test_file: str) -> None: def test_layer_crashes(test_file: str) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
with Image.open(f) as im: with Image.open(f) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
im.layers im.layers

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,31 +72,42 @@ def test_invalid_file() -> None:
SgiImagePlugin.SgiImageFile(invalid_file) SgiImagePlugin.SgiImageFile(invalid_file)
def test_write(tmp_path: Path) -> None: def test_unsupported_image_mode() -> None:
def roundtrip(img: Image.Image) -> None: with open("Tests/images/hopper.rgb", "rb") as fp:
out = str(tmp_path / "temp.sgi") 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:
out = tmp_path / "temp.sgi"
img.save(out, format="sgi") img.save(out, format="sgi")
assert_image_equal_tofile(img, out) assert_image_equal_tofile(img, out)
out = str(tmp_path / "fp.sgi") out = tmp_path / "fp.sgi"
with open(out, "wb") as fp: with open(out, "wb") as fp:
img.save(fp) img.save(fp)
assert_image_equal_tofile(img, out) assert_image_equal_tofile(img, out)
assert not fp.closed assert not fp.closed
for mode in ("L", "RGB", "RGBA"):
roundtrip(hopper(mode))
# Test 1 dimension for an L mode image @pytest.mark.parametrize("mode", ("L", "RGB", "RGBA"))
roundtrip(Image.new("L", (10, 1))) def test_write(mode: str, tmp_path: Path) -> None:
roundtrip(hopper(mode), tmp_path)
def test_write_L_mode_1_dimension(tmp_path: Path) -> None:
roundtrip(Image.new("L", (10, 1)), tmp_path)
def test_write16(tmp_path: Path) -> None: def test_write16(tmp_path: Path) -> None:
test_file = "Tests/images/hopper16.rgb" test_file = "Tests/images/hopper16.rgb"
with Image.open(test_file) as im: with Image.open(test_file) as im:
out = str(tmp_path / "temp.sgi") out = tmp_path / "temp.sgi"
im.save(out, format="sgi", bpc=2) im.save(out, format="sgi", bpc=2)
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
@ -103,7 +115,15 @@ def test_write16(tmp_path: Path) -> None:
def test_unsupported_mode(tmp_path: Path) -> None: def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("LA") im = hopper("LA")
out = str(tmp_path / "temp.sgi") out = tmp_path / "temp.sgi"
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

@ -24,12 +24,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None: def test_unclosed_file() -> None:
def open() -> None: def open_test_image() -> None:
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
with pytest.warns(ResourceWarning): with pytest.warns(ResourceWarning):
open() open_test_image()
def test_closed_file() -> None: def test_closed_file() -> None:
@ -51,7 +51,7 @@ def test_context_manager() -> None:
def test_save(tmp_path: Path) -> None: def test_save(tmp_path: Path) -> None:
# Arrange # Arrange
temp = str(tmp_path / "temp.spider") temp = tmp_path / "temp.spider"
im = hopper() im = hopper()
# Act # Act
@ -96,6 +96,7 @@ def test_tell() -> None:
def test_n_frames() -> None: def test_n_frames() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert isinstance(im, SpiderImagePlugin.SpiderImageFile)
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
import io
import os import os
import pytest import pytest
from PIL import Image, SunImagePlugin from PIL import Image, SunImagePlugin, _binary
from .helper import assert_image_equal_tofile, assert_image_similar, hopper from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -33,6 +34,60 @@ def test_im1() -> None:
assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png")
def _sun_header(
depth: int = 0, file_type: int = 0, palette_length: int = 0
) -> io.BytesIO:
return io.BytesIO(
_binary.o32be(0x59A66A95)
+ b"\x00" * 8
+ _binary.o32be(depth)
+ b"\x00" * 4
+ _binary.o32be(file_type)
+ b"\x00" * 4
+ _binary.o32be(palette_length)
)
def test_unsupported_mode_bit_depth() -> None:
with pytest.raises(SyntaxError, match="Unsupported Mode/Bit Depth"):
with SunImagePlugin.SunImageFile(_sun_header()):
pass
def test_unsupported_color_palette_length() -> None:
with pytest.raises(SyntaxError, match="Unsupported Color Palette Length"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1025)):
pass
def test_unsupported_palette_type() -> None:
with pytest.raises(SyntaxError, match="Unsupported Palette Type"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1)):
pass
def test_unsupported_file_type() -> None:
with pytest.raises(SyntaxError, match="Unsupported Sun Raster file type"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, file_type=6)):
pass
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_rgbx() -> None:
with open(os.path.join(EXTRA_DIR, "32bpp.ras"), "rb") as fp:
data = fp.read()
# Set file type to 3
data = data[:20] + _binary.o32be(3) + data[24:]
with Image.open(io.BytesIO(data)) as im:
r, g, b = im.split()
im = Image.merge("RGB", (b, g, r))
assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
@pytest.mark.skipif( @pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
) )

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