mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-07-10 16:22:22 +03:00
Merge branch 'main' into msys
This commit is contained in:
commit
dcab6e6b94
|
@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
|
||||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
||||||
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
||||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||||
sway wl-clipboard libopenblas-dev
|
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
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
cibuildwheel==2.23.1
|
cibuildwheel==2.23.3
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
mypy==1.15.0
|
mypy==1.16.0
|
||||||
IceSpringPySideStubs-PyQt6
|
IceSpringPySideStubs-PyQt6
|
||||||
IceSpringPySideStubs-PySide6
|
IceSpringPySideStubs-PySide6
|
||||||
ipython
|
ipython
|
||||||
numpy
|
numpy
|
||||||
packaging
|
packaging
|
||||||
|
pyarrow-stubs
|
||||||
pytest
|
pytest
|
||||||
sphinx
|
sphinx
|
||||||
types-atheris
|
types-atheris
|
||||||
|
|
|
@ -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
46
.github/ISSUE_TEMPLATE/RELEASE.md
vendored
Normal 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]]
|
||||||
|
```
|
10
.github/workflows/macos-install.sh
vendored
10
.github/workflows/macos-install.sh
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/test-docker.yml
vendored
2
.github/workflows/test-docker.yml
vendored
|
@ -47,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,
|
||||||
|
|
60
.github/workflows/test-valgrind-memory.yml
vendored
Normal file
60
.github/workflows/test-valgrind-memory.yml
vendored
Normal 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
|
21
.github/workflows/test-windows.yml
vendored
21
.github/workflows/test-windows.yml
vendored
|
@ -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.11", "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", "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.5.0 --no-progress
|
choco install ghostscript --version=10.5.1 --no-progress
|
||||||
echo "C:\Program Files\gs\gs10.05.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'
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -70,7 +70,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
|
||||||
|
|
23
.github/workflows/wheels-dependencies.sh
vendored
23
.github/workflows/wheels-dependencies.sh
vendored
|
@ -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,13 +38,14 @@ 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.4.0
|
HARFBUZZ_VERSION=11.2.1
|
||||||
LIBPNG_VERSION=1.6.47
|
LIBPNG_VERSION=1.6.48
|
||||||
JPEGTURBO_VERSION=3.1.0
|
JPEGTURBO_VERSION=3.1.0
|
||||||
OPENJPEG_VERSION=2.5.3
|
OPENJPEG_VERSION=2.5.3
|
||||||
XZ_VERSION=5.6.4
|
XZ_VERSION=5.8.1
|
||||||
TIFF_VERSION=4.7.0
|
TIFF_VERSION=4.7.0
|
||||||
LCMS2_VERSION=2.17
|
LCMS2_VERSION=2.17
|
||||||
|
ZLIB_VERSION=1.3.1
|
||||||
ZLIB_NG_VERSION=2.2.4
|
ZLIB_NG_VERSION=2.2.4
|
||||||
LIBWEBP_VERSION=1.5.0
|
LIBWEBP_VERSION=1.5.0
|
||||||
BZIP2_VERSION=1.0.8
|
BZIP2_VERSION=1.0.8
|
||||||
|
@ -64,11 +65,7 @@ function build_pkg_config {
|
||||||
|
|
||||||
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 \
|
|
||||||
&& make -j4 \
|
|
||||||
&& make install)
|
|
||||||
|
|
||||||
if [ -n "$IS_MACOS" ]; then
|
if [ -n "$IS_MACOS" ]; then
|
||||||
# Ensure that on macOS, the library name is an absolute path, not an
|
# Ensure that on macOS, the library name is an absolute path, not an
|
||||||
|
@ -95,7 +92,7 @@ function build_harfbuzz {
|
||||||
|
|
||||||
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
|
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
|
||||||
|
@ -106,7 +103,11 @@ function build {
|
||||||
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
|
||||||
build_zlib_ng
|
if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then
|
||||||
|
build_new_zlib
|
||||||
|
else
|
||||||
|
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
|
||||||
|
|
9
.github/workflows/wheels.yml
vendored
9
.github/workflows/wheels.yml
vendored
|
@ -121,14 +121,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:
|
||||||
|
@ -157,7 +160,7 @@ jobs:
|
||||||
# Install extra test images
|
# Install extra test images
|
||||||
xcopy /S /Y Tests\test-images\* Tests\images
|
xcopy /S /Y Tests\test-images\* Tests\images
|
||||||
|
|
||||||
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
|
& python.exe winbuild\build_prepare.py -v --no-imagequant --no-avif --architecture=${{ matrix.cibw_arch }}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
|
@ -240,7 +243,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
7
.github/zizmor.yml
vendored
Normal 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
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.9.9
|
rev: v0.11.12
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--exit-non-zero-on-fix]
|
args: [--exit-non-zero-on-fix]
|
||||||
|
@ -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.5
|
||||||
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.2
|
rev: 0.33.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-github-workflows
|
- id: check-github-workflows
|
||||||
- id: check-readthedocs
|
- id: check-readthedocs
|
||||||
- id: check-renovate
|
- id: check-renovate
|
||||||
|
|
||||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||||
rev: v1.4.1
|
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.1
|
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]
|
||||||
|
|
21
Makefile
21
Makefile
|
@ -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
|
||||||
|
|
|
@ -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).
|
||||||
|
|
35
RELEASING.md
35
RELEASING.md
|
@ -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
|
||||||
|
|
|
@ -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``.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
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"}
|
||||||
|
|
||||||
# tkinter is not available in cibuildwheel installed CPython on Windows
|
if sys.platform == "win32":
|
||||||
try:
|
# tkinter is not available in cibuildwheel installed CPython on Windows
|
||||||
import tkinter
|
try:
|
||||||
|
import tkinter
|
||||||
|
|
||||||
assert tkinter
|
assert tkinter
|
||||||
except ImportError:
|
except ImportError:
|
||||||
expected_modules.remove("tkinter")
|
expected_modules.remove("tkinter")
|
||||||
|
|
||||||
assert set(features.get_supported_modules()) == expected_modules
|
assert set(features.get_supported_modules()) == expected_modules
|
||||||
|
|
||||||
|
@ -40,5 +44,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
|
||||||
|
|
|
@ -13,6 +13,7 @@ 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
|
||||||
|
@ -95,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:
|
||||||
|
@ -136,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:
|
||||||
|
@ -157,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)
|
||||||
|
|
BIN
Tests/images/avif/exif.avif
Normal file
BIN
Tests/images/avif/exif.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper-missing-pixi.avif
Normal file
BIN
Tests/images/avif/hopper-missing-pixi.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper.avif
Normal file
BIN
Tests/images/avif/hopper.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper.heif
Normal file
BIN
Tests/images/avif/hopper.heif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper_avif_write.png
Normal file
BIN
Tests/images/avif/hopper_avif_write.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
Tests/images/avif/icc_profile.avif
Normal file
BIN
Tests/images/avif/icc_profile.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/icc_profile_none.avif
Normal file
BIN
Tests/images/avif/icc_profile_none.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot0mir0.avif
Normal file
BIN
Tests/images/avif/rot0mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot0mir1.avif
Normal file
BIN
Tests/images/avif/rot0mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot1mir0.avif
Normal file
BIN
Tests/images/avif/rot1mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot1mir1.avif
Normal file
BIN
Tests/images/avif/rot1mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot2mir0.avif
Normal file
BIN
Tests/images/avif/rot2mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot2mir1.avif
Normal file
BIN
Tests/images/avif/rot2mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot3mir0.avif
Normal file
BIN
Tests/images/avif/rot3mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot3mir1.avif
Normal file
BIN
Tests/images/avif/rot3mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/star.avifs
Normal file
BIN
Tests/images/avif/star.avifs
Normal file
Binary file not shown.
BIN
Tests/images/avif/star.gif
Normal file
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
BIN
Tests/images/avif/star.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
BIN
Tests/images/avif/transparency.avif
Normal file
BIN
Tests/images/avif/transparency.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/xmp_tags_orientation.avif
Normal file
BIN
Tests/images/avif/xmp_tags_orientation.avif
Normal file
Binary file not shown.
BIN
Tests/images/drawing_emf_ref_72_144.png
Normal file
BIN
Tests/images/drawing_emf_ref_72_144.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 984 B |
260
Tests/images/full_gimp_palette.gpl
Normal file
260
Tests/images/full_gimp_palette.gpl
Normal 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
|
Binary file not shown.
Before Width: | Height: | Size: 533 B After Width: | Height: | Size: 533 B |
|
@ -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
164
Tests/test_arrow.py
Normal 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)
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -52,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)
|
||||||
|
@ -59,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)
|
||||||
|
@ -91,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)
|
||||||
|
@ -132,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)
|
||||||
|
|
||||||
|
@ -145,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)
|
||||||
|
@ -178,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)
|
||||||
|
@ -233,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")
|
||||||
|
@ -258,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")
|
||||||
|
@ -265,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")
|
||||||
|
@ -274,25 +300,31 @@ 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):
|
||||||
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
|
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
@ -300,26 +332,31 @@ def test_apng_chunk_errors() -> None:
|
||||||
def test_apng_syntax_errors() -> None:
|
def test_apng_syntax_errors() -> None:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
||||||
|
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()
|
||||||
|
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
# 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):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
@ -339,16 +376,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
|
||||||
|
@ -364,6 +403,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
|
||||||
|
@ -375,7 +415,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))
|
||||||
|
@ -393,7 +433,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(
|
||||||
|
@ -403,12 +443,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 = []
|
||||||
|
@ -445,6 +486,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
|
||||||
|
|
||||||
|
@ -456,6 +498,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
|
||||||
|
|
||||||
|
@ -466,12 +509,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))
|
||||||
|
@ -572,7 +616,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))
|
||||||
|
@ -594,7 +638,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))
|
||||||
|
@ -662,7 +706,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))])
|
||||||
|
@ -686,7 +730,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(
|
||||||
|
@ -700,7 +744,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):
|
||||||
|
|
779
Tests/test_file_avif.py
Normal file
779
Tests/test_file_avif.py
Normal file
|
@ -0,0 +1,779 @@
|
||||||
|
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.warns(UserWarning):
|
||||||
|
with pytest.raises(UnidentifiedImageError):
|
||||||
|
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
|
||||||
|
assert im.getchannel("A").getcolors()[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)
|
|
@ -46,7 +46,7 @@ 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")
|
||||||
|
@ -56,7 +56,7 @@ def test_save(tmp_path: Path) -> None:
|
||||||
assert_image_equal(im.convert("RGB"), reloaded)
|
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:
|
with Image.open(f) as reloaded:
|
||||||
|
|
|
@ -15,25 +15,19 @@ 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.save(outfile, "BMP")
|
im = hopper(mode)
|
||||||
|
im.save(outfile, "BMP")
|
||||||
|
|
||||||
with Image.open(outfile) as reloaded:
|
with Image.open(outfile) as reloaded:
|
||||||
reloaded.load()
|
reloaded.load()
|
||||||
assert im.mode == reloaded.mode
|
assert im.mode == reloaded.mode
|
||||||
assert im.size == reloaded.size
|
assert im.size == reloaded.size
|
||||||
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:
|
||||||
|
@ -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)
|
||||||
|
@ -196,9 +190,9 @@ def test_rle8() -> None:
|
||||||
# Signal end of bitmap before the image is finished
|
# Signal end of bitmap before the image is finished
|
||||||
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
|
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
|
||||||
data = fp.read(1063) + b"\x01"
|
data = fp.read(1063) + b"\x01"
|
||||||
with Image.open(io.BytesIO(data)) as im:
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_rle4() -> None:
|
def test_rle4() -> None:
|
||||||
|
@ -220,9 +214,9 @@ def test_rle4() -> None:
|
||||||
def test_rle8_eof(file_name: str, length: int) -> None:
|
def test_rle8_eof(file_name: str, length: int) -> None:
|
||||||
with open(file_name, "rb") as fp:
|
with open(file_name, "rb") as fp:
|
||||||
data = fp.read(length)
|
data = fp.read(length)
|
||||||
with Image.open(io.BytesIO(data)) as im:
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_offset() -> None:
|
def test_offset() -> None:
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
(
|
(
|
||||||
|
@ -368,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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -384,10 +416,98 @@ 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)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
assert_image_equal_tofile(im, out)
|
||||||
assert_image_equal(im, reloaded)
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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")
|
||||||
|
|
|
@ -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
|
||||||
|
@ -227,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"
|
||||||
|
|
||||||
|
@ -239,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")
|
||||||
|
|
||||||
|
|
||||||
|
@ -274,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)
|
||||||
|
|
||||||
|
@ -308,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")
|
||||||
|
@ -316,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")
|
||||||
|
@ -392,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"],
|
||||||
|
|
|
@ -7,7 +7,12 @@ 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.
|
||||||
|
@ -22,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)
|
||||||
|
@ -29,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"
|
||||||
|
@ -112,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
|
||||||
|
@ -166,6 +178,7 @@ 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")
|
||||||
|
@ -181,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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -228,7 +228,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None:
|
||||||
|
|
||||||
|
|
||||||
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))
|
||||||
|
@ -249,7 +249,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:
|
||||||
|
@ -258,7 +258,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)
|
||||||
|
@ -268,7 +268,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:
|
||||||
|
@ -276,7 +276,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:
|
||||||
|
@ -284,7 +284,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])
|
||||||
|
@ -329,7 +329,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:
|
||||||
|
@ -345,7 +345,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:
|
||||||
|
@ -356,7 +356,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)
|
||||||
|
|
||||||
|
@ -402,6 +402,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:
|
||||||
|
@ -446,10 +447,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)
|
||||||
|
|
||||||
|
@ -459,6 +462,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)
|
||||||
|
|
||||||
|
@ -466,17 +470,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
|
||||||
|
@ -495,6 +502,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)
|
||||||
|
@ -518,6 +526,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)
|
||||||
|
@ -571,6 +580,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)
|
||||||
|
@ -599,7 +609,7 @@ 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"),
|
||||||
|
@ -608,6 +618,7 @@ def test_save_dispose(tmp_path: Path) -> None:
|
||||||
for method in range(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
|
||||||
|
@ -621,13 +632,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)]
|
||||||
|
@ -661,7 +673,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 = [
|
||||||
|
@ -703,7 +715,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 = []
|
||||||
|
|
||||||
|
@ -729,7 +741,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))]
|
||||||
|
|
||||||
|
@ -743,11 +755,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
|
||||||
|
@ -766,7 +779,7 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dispose2_without_transparency(tmp_path: Path) -> None:
|
def test_dispose2_without_transparency(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))
|
||||||
|
|
||||||
|
@ -781,7 +794,7 @@ def test_dispose2_without_transparency(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -811,7 +824,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()
|
||||||
|
@ -829,7 +842,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
|
||||||
|
@ -843,7 +856,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"),
|
||||||
|
@ -878,7 +891,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)
|
||||||
|
@ -893,7 +906,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,
|
||||||
|
@ -911,7 +924,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"),
|
||||||
|
@ -924,6 +937,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
|
||||||
|
|
||||||
|
@ -944,7 +959,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"),
|
||||||
|
@ -953,6 +968,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
|
||||||
|
|
||||||
|
@ -961,7 +978,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:
|
||||||
|
@ -971,7 +988,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:
|
||||||
|
@ -987,7 +1004,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)
|
||||||
|
@ -996,7 +1013,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"):
|
||||||
|
@ -1014,7 +1031,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)
|
||||||
|
@ -1031,7 +1048,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:
|
||||||
|
@ -1057,7 +1074,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
|
||||||
|
|
||||||
|
@ -1091,7 +1108,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")
|
||||||
|
|
||||||
|
@ -1101,7 +1118,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)
|
||||||
|
@ -1131,7 +1148,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")
|
||||||
|
@ -1139,6 +1156,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
|
||||||
|
@ -1148,6 +1173,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
|
||||||
|
@ -1156,11 +1182,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")
|
||||||
|
@ -1187,7 +1214,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:
|
||||||
|
@ -1195,7 +1222,7 @@ 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):
|
||||||
|
@ -1210,7 +1237,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))
|
||||||
|
@ -1232,7 +1259,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)])
|
||||||
|
@ -1242,25 +1269,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))
|
||||||
|
@ -1268,6 +1296,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
|
||||||
|
|
||||||
|
|
||||||
|
@ -1279,7 +1308,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:
|
||||||
|
@ -1290,7 +1319,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:
|
||||||
|
@ -1306,7 +1335,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:
|
||||||
|
@ -1321,7 +1350,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:]
|
||||||
)
|
)
|
||||||
|
@ -1344,7 +1373,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:
|
||||||
|
@ -1357,7 +1386,7 @@ 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:
|
||||||
|
@ -1419,6 +1448,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
|
||||||
|
@ -1441,7 +1471,7 @@ 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)
|
||||||
|
|
||||||
|
@ -1452,7 +1482,7 @@ def test_saving_rgba(tmp_path: Path) -> None:
|
||||||
|
|
||||||
@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)
|
||||||
|
@ -1466,4 +1496,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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ def test_load() -> None:
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
@ -60,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:
|
||||||
|
@ -69,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)
|
||||||
|
@ -90,6 +91,7 @@ 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
|
||||||
|
@ -118,6 +120,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"
|
||||||
|
@ -135,6 +138,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"
|
||||||
|
|
|
@ -41,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)
|
||||||
|
@ -77,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
|
||||||
|
@ -88,12 +89,13 @@ 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)
|
||||||
|
|
||||||
|
@ -101,8 +103,8 @@ def test_getpixel(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
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)]
|
||||||
|
@ -115,8 +117,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)])
|
||||||
|
@ -132,8 +134,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(
|
||||||
|
@ -167,6 +169,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
|
||||||
|
@ -178,6 +181,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)
|
||||||
|
|
||||||
|
@ -186,7 +190,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)
|
||||||
|
@ -202,7 +206,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)
|
||||||
|
|
||||||
|
@ -215,10 +219,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)
|
||||||
|
@ -235,7 +240,7 @@ def test_unexpected_size() -> None:
|
||||||
|
|
||||||
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")
|
||||||
|
|
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
@ -143,14 +145,16 @@ class TestFileJpeg:
|
||||||
assert k > 0.9
|
assert k > 0.9
|
||||||
# roundtrip, and check again
|
# roundtrip, and check again
|
||||||
im = self.roundtrip(im)
|
im = self.roundtrip(im)
|
||||||
c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
|
cmyk = im.getpixel((0, 0))
|
||||||
|
assert isinstance(cmyk, tuple)
|
||||||
|
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
|
||||||
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
|
||||||
|
|
||||||
def test_rgb(self) -> None:
|
def test_rgb(self) -> None:
|
||||||
|
@ -194,7 +198,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 +242,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 +254,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 +272,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 +292,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 +311,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 +320,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 +341,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 +506,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 +565,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 +672,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 +715,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 +737,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:
|
||||||
|
@ -762,7 +773,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 +784,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 +870,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 +920,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 +1017,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 +1036,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
|
||||||
|
@ -1084,6 +1096,7 @@ 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:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
assert im.huffman_ac == {}
|
assert im.huffman_ac == {}
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
|
@ -1094,7 +1107,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)
|
||||||
|
|
|
@ -99,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)
|
||||||
|
|
||||||
|
@ -213,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)
|
||||||
|
|
||||||
|
@ -228,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)
|
||||||
|
@ -289,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)
|
||||||
|
@ -455,8 +457,8 @@ def test_comment() -> None:
|
||||||
# Test an image that is truncated partway through a codestream
|
# Test an image that is truncated partway through a codestream
|
||||||
with open("Tests/images/comment.jp2", "rb") as fp:
|
with open("Tests/images/comment.jp2", "rb") as fp:
|
||||||
b = BytesIO(fp.read(130))
|
b = BytesIO(fp.read(130))
|
||||||
with Image.open(b) as im:
|
with Image.open(b) as im:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_save_comment(card: ImageFile.ImageFile) -> None:
|
def test_save_comment(card: ImageFile.ImageFile) -> None:
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -80,7 +81,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
s = io.BytesIO()
|
s = io.BytesIO()
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
s.write(f.read())
|
s.write(f.read())
|
||||||
s.seek(0)
|
s.seek(0)
|
||||||
with Image.open(s) as im:
|
with Image.open(s) as im:
|
||||||
assert im.size == (500, 500)
|
assert im.size == (500, 500)
|
||||||
self._assert_noerr(tmp_path, im)
|
self._assert_noerr(tmp_path, im)
|
||||||
|
@ -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()
|
||||||
|
@ -1147,7 +1179,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
@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:
|
||||||
|
@ -1160,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)
|
||||||
|
@ -1176,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")
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ class TestFileLibTiffSmall(LibTiffTestCase):
|
||||||
s = BytesIO()
|
s = BytesIO()
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
s.write(f.read())
|
s.write(f.read())
|
||||||
s.seek(0)
|
s.seek(0)
|
||||||
with Image.open(s) as im:
|
with Image.open(s) as im:
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
self._assert_noerr(tmp_path, im)
|
self._assert_noerr(tmp_path, im)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -80,6 +80,7 @@ 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 im.applist[1][1].startswith(
|
assert im.applist[1][1].startswith(
|
||||||
|
@ -220,12 +221,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
|
||||||
|
@ -239,6 +242,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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -11,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
|
||||||
|
@ -31,7 +31,7 @@ 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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
|
|
@ -94,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)
|
||||||
|
|
||||||
|
@ -106,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)
|
||||||
|
@ -117,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)
|
||||||
|
@ -128,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)
|
||||||
|
@ -194,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)
|
||||||
|
@ -207,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)
|
||||||
|
|
||||||
|
@ -218,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)
|
||||||
|
|
||||||
|
@ -235,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)
|
||||||
|
|
||||||
|
@ -245,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")
|
||||||
|
|
||||||
|
@ -255,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")
|
||||||
|
|
||||||
|
@ -270,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")
|
||||||
|
|
||||||
|
@ -279,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")
|
||||||
|
|
||||||
|
@ -289,7 +289,7 @@ def test_non_integer_token(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_header_token_too_long(tmp_path: Path) -> None:
|
def test_header_token_too_long(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\n 01234567890")
|
f.write(b"P6\n 01234567890")
|
||||||
|
|
||||||
|
@ -300,7 +300,7 @@ def test_header_token_too_long(tmp_path: Path) -> None:
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
|
@ -316,7 +316,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")
|
||||||
|
|
||||||
|
@ -327,7 +327,7 @@ 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)
|
||||||
|
|
||||||
|
@ -350,7 +350,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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -71,31 +71,33 @@ def test_invalid_file() -> None:
|
||||||
SgiImagePlugin.SgiImageFile(invalid_file)
|
SgiImagePlugin.SgiImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_write(tmp_path: Path) -> None:
|
def roundtrip(img: Image.Image, tmp_path: Path) -> None:
|
||||||
def roundtrip(img: Image.Image) -> None:
|
out = tmp_path / "temp.sgi"
|
||||||
out = str(tmp_path / "temp.sgi")
|
img.save(out, format="sgi")
|
||||||
img.save(out, format="sgi")
|
assert_image_equal_tofile(img, out)
|
||||||
|
|
||||||
|
out = tmp_path / "fp.sgi"
|
||||||
|
with open(out, "wb") as fp:
|
||||||
|
img.save(fp)
|
||||||
assert_image_equal_tofile(img, out)
|
assert_image_equal_tofile(img, out)
|
||||||
|
|
||||||
out = str(tmp_path / "fp.sgi")
|
assert not fp.closed
|
||||||
with open(out, "wb") as fp:
|
|
||||||
img.save(fp)
|
|
||||||
assert_image_equal_tofile(img, out)
|
|
||||||
|
|
||||||
assert not fp.closed
|
|
||||||
|
|
||||||
for mode in ("L", "RGB", "RGBA"):
|
@pytest.mark.parametrize("mode", ("L", "RGB", "RGBA"))
|
||||||
roundtrip(hopper(mode))
|
def test_write(mode: str, tmp_path: Path) -> None:
|
||||||
|
roundtrip(hopper(mode), tmp_path)
|
||||||
|
|
||||||
# Test 1 dimension for an L mode image
|
|
||||||
roundtrip(Image.new("L", (10, 1)))
|
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 +105,7 @@ 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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from glob import glob
|
|
||||||
from itertools import product
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -15,16 +13,29 @@ _TGA_DIR = os.path.join("Tests", "images", "tga")
|
||||||
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
|
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
|
||||||
|
|
||||||
|
|
||||||
_MODES = ("L", "LA", "P", "RGB", "RGBA")
|
|
||||||
_ORIGINS = ("tl", "bl")
|
_ORIGINS = ("tl", "bl")
|
||||||
|
|
||||||
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", _MODES)
|
@pytest.mark.parametrize(
|
||||||
def test_sanity(mode: str, tmp_path: Path) -> None:
|
"size_mode",
|
||||||
|
(
|
||||||
|
("1x1", "L"),
|
||||||
|
("200x32", "L"),
|
||||||
|
("200x32", "LA"),
|
||||||
|
("200x32", "P"),
|
||||||
|
("200x32", "RGB"),
|
||||||
|
("200x32", "RGBA"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize("origin", _ORIGINS)
|
||||||
|
@pytest.mark.parametrize("rle", (True, False))
|
||||||
|
def test_sanity(
|
||||||
|
size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
def roundtrip(original_im: Image.Image) -> None:
|
def roundtrip(original_im: Image.Image) -> None:
|
||||||
out = str(tmp_path / "temp.tga")
|
out = tmp_path / "temp.tga"
|
||||||
|
|
||||||
original_im.save(out, rle=rle)
|
original_im.save(out, rle=rle)
|
||||||
with Image.open(out) as saved_im:
|
with Image.open(out) as saved_im:
|
||||||
|
@ -36,36 +47,29 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||||
|
|
||||||
assert_image_equal(saved_im, original_im)
|
assert_image_equal(saved_im, original_im)
|
||||||
|
|
||||||
png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png"))
|
size, mode = size_mode
|
||||||
|
png_path = os.path.join(_TGA_DIR_COMMON, size + "_" + mode.lower() + ".png")
|
||||||
|
with Image.open(png_path) as reference_im:
|
||||||
|
assert reference_im.mode == mode
|
||||||
|
|
||||||
for png_path in png_paths:
|
path_no_ext = os.path.splitext(png_path)[0]
|
||||||
with Image.open(png_path) as reference_im:
|
tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw")
|
||||||
assert reference_im.mode == mode
|
|
||||||
|
|
||||||
path_no_ext = os.path.splitext(png_path)[0]
|
with Image.open(tga_path) as original_im:
|
||||||
for origin, rle in product(_ORIGINS, (True, False)):
|
assert original_im.format == "TGA"
|
||||||
tga_path = "{}_{}_{}.tga".format(
|
assert original_im.get_format_mimetype() == "image/x-tga"
|
||||||
path_no_ext, origin, "rle" if rle else "raw"
|
if rle:
|
||||||
)
|
assert original_im.info["compression"] == "tga_rle"
|
||||||
|
assert original_im.info["orientation"] == _ORIGIN_TO_ORIENTATION[origin]
|
||||||
|
if mode == "P":
|
||||||
|
assert original_im.getpalette() == reference_im.getpalette()
|
||||||
|
|
||||||
with Image.open(tga_path) as original_im:
|
assert_image_equal(original_im, reference_im)
|
||||||
assert original_im.format == "TGA"
|
|
||||||
assert original_im.get_format_mimetype() == "image/x-tga"
|
|
||||||
if rle:
|
|
||||||
assert original_im.info["compression"] == "tga_rle"
|
|
||||||
assert (
|
|
||||||
original_im.info["orientation"]
|
|
||||||
== _ORIGIN_TO_ORIENTATION[origin]
|
|
||||||
)
|
|
||||||
if mode == "P":
|
|
||||||
assert original_im.getpalette() == reference_im.getpalette()
|
|
||||||
|
|
||||||
assert_image_equal(original_im, reference_im)
|
roundtrip(original_im)
|
||||||
|
|
||||||
roundtrip(original_im)
|
|
||||||
|
|
||||||
|
|
||||||
def test_palette_depth_8(tmp_path: Path) -> None:
|
def test_palette_depth_8() -> None:
|
||||||
with pytest.raises(UnidentifiedImageError):
|
with pytest.raises(UnidentifiedImageError):
|
||||||
Image.open("Tests/images/p_8.tga")
|
Image.open("Tests/images/p_8.tga")
|
||||||
|
|
||||||
|
@ -76,7 +80,7 @@ def test_palette_depth_16(tmp_path: Path) -> None:
|
||||||
assert im.palette.mode == "RGBA"
|
assert im.palette.mode == "RGBA"
|
||||||
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
|
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
|
||||||
|
|
||||||
out = str(tmp_path / "temp.png")
|
out = tmp_path / "temp.png"
|
||||||
im.save(out)
|
im.save(out)
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
|
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
|
||||||
|
@ -122,7 +126,7 @@ def test_cross_scan_line() -> None:
|
||||||
def test_save(tmp_path: Path) -> None:
|
def test_save(tmp_path: Path) -> None:
|
||||||
test_file = "Tests/images/tga_id_field.tga"
|
test_file = "Tests/images/tga_id_field.tga"
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
out = str(tmp_path / "temp.tga")
|
out = tmp_path / "temp.tga"
|
||||||
|
|
||||||
# Save
|
# Save
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
@ -141,7 +145,7 @@ def test_small_palette(tmp_path: Path) -> None:
|
||||||
colors = [0, 0, 0]
|
colors = [0, 0, 0]
|
||||||
im.putpalette(colors)
|
im.putpalette(colors)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tga")
|
out = tmp_path / "temp.tga"
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
@ -155,7 +159,7 @@ def test_missing_palette() -> None:
|
||||||
|
|
||||||
def test_save_wrong_mode(tmp_path: Path) -> None:
|
def test_save_wrong_mode(tmp_path: Path) -> None:
|
||||||
im = hopper("PA")
|
im = hopper("PA")
|
||||||
out = str(tmp_path / "temp.tga")
|
out = tmp_path / "temp.tga"
|
||||||
|
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
@ -172,7 +176,7 @@ def test_save_mapdepth() -> None:
|
||||||
def test_save_id_section(tmp_path: Path) -> None:
|
def test_save_id_section(tmp_path: Path) -> None:
|
||||||
test_file = "Tests/images/rgb32rle.tga"
|
test_file = "Tests/images/rgb32rle.tga"
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
out = str(tmp_path / "temp.tga")
|
out = tmp_path / "temp.tga"
|
||||||
|
|
||||||
# Check there is no id section
|
# Check there is no id section
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
@ -202,7 +206,7 @@ def test_save_id_section(tmp_path: Path) -> None:
|
||||||
|
|
||||||
def test_save_orientation(tmp_path: Path) -> None:
|
def test_save_orientation(tmp_path: Path) -> None:
|
||||||
test_file = "Tests/images/rgb32rle.tga"
|
test_file = "Tests/images/rgb32rle.tga"
|
||||||
out = str(tmp_path / "temp.tga")
|
out = tmp_path / "temp.tga"
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert im.info["orientation"] == -1
|
assert im.info["orientation"] == -1
|
||||||
|
|
||||||
|
@ -229,7 +233,7 @@ def test_save_rle(tmp_path: Path) -> None:
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert im.info["compression"] == "tga_rle"
|
assert im.info["compression"] == "tga_rle"
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tga")
|
out = tmp_path / "temp.tga"
|
||||||
|
|
||||||
# Save
|
# Save
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
@ -266,7 +270,7 @@ def test_save_l_transparency(tmp_path: Path) -> None:
|
||||||
assert im.mode == "LA"
|
assert im.mode == "LA"
|
||||||
assert im.getchannel("A").getcolors()[0][0] == num_transparent
|
assert im.getchannel("A").getcolors()[0][0] == num_transparent
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tga")
|
out = tmp_path / "temp.tga"
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
||||||
with Image.open(out) as test_im:
|
with Image.open(out) as test_im:
|
||||||
|
|
|
@ -9,7 +9,13 @@ from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError
|
from PIL import (
|
||||||
|
Image,
|
||||||
|
ImageFile,
|
||||||
|
JpegImagePlugin,
|
||||||
|
TiffImagePlugin,
|
||||||
|
UnidentifiedImageError,
|
||||||
|
)
|
||||||
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
|
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
|
@ -20,6 +26,7 @@ from .helper import (
|
||||||
hopper,
|
hopper,
|
||||||
is_pypy,
|
is_pypy,
|
||||||
is_win32,
|
is_win32,
|
||||||
|
timeout_unless_slower_valgrind,
|
||||||
)
|
)
|
||||||
|
|
||||||
ElementTree: ModuleType | None
|
ElementTree: ModuleType | None
|
||||||
|
@ -31,7 +38,7 @@ except ImportError:
|
||||||
|
|
||||||
class TestFileTiff:
|
class TestFileTiff:
|
||||||
def test_sanity(self, tmp_path: Path) -> None:
|
def test_sanity(self, tmp_path: Path) -> None:
|
||||||
filename = str(tmp_path / "temp.tif")
|
filename = tmp_path / "temp.tif"
|
||||||
|
|
||||||
hopper("RGB").save(filename)
|
hopper("RGB").save(filename)
|
||||||
|
|
||||||
|
@ -112,20 +119,23 @@ class TestFileTiff:
|
||||||
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
||||||
|
|
||||||
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||||
|
|
||||||
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
im = hopper()
|
im = hopper()
|
||||||
im.save(outfile, big_tiff=True)
|
im.save(outfile, big_tiff=True)
|
||||||
|
|
||||||
with Image.open(outfile) as reloaded:
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2._bigtiff is True
|
assert reloaded.tag_v2._bigtiff is True
|
||||||
|
|
||||||
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
|
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
|
||||||
|
|
||||||
with Image.open(outfile) as reloaded:
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2._bigtiff is True
|
assert reloaded.tag_v2._bigtiff is True
|
||||||
|
|
||||||
def test_seek_too_large(self) -> None:
|
def test_seek_too_large(self) -> None:
|
||||||
|
@ -140,6 +150,8 @@ class TestFileTiff:
|
||||||
def test_xyres_tiff(self) -> None:
|
def test_xyres_tiff(self) -> None:
|
||||||
filename = "Tests/images/pil168.tif"
|
filename = "Tests/images/pil168.tif"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# legacy api
|
# legacy api
|
||||||
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
|
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
|
||||||
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
|
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
|
||||||
|
@ -153,6 +165,8 @@ class TestFileTiff:
|
||||||
def test_xyres_fallback_tiff(self) -> None:
|
def test_xyres_fallback_tiff(self) -> None:
|
||||||
filename = "Tests/images/compression.tif"
|
filename = "Tests/images/compression.tif"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# v2 api
|
# v2 api
|
||||||
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
|
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
|
||||||
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
|
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
|
||||||
|
@ -167,6 +181,8 @@ class TestFileTiff:
|
||||||
def test_int_resolution(self) -> None:
|
def test_int_resolution(self) -> None:
|
||||||
filename = "Tests/images/pil168.tif"
|
filename = "Tests/images/pil168.tif"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# Try to read a file where X,Y_RESOLUTION are ints
|
# Try to read a file where X,Y_RESOLUTION are ints
|
||||||
im.tag_v2[X_RESOLUTION] = 71
|
im.tag_v2[X_RESOLUTION] = 71
|
||||||
im.tag_v2[Y_RESOLUTION] = 71
|
im.tag_v2[Y_RESOLUTION] = 71
|
||||||
|
@ -181,11 +197,12 @@ class TestFileTiff:
|
||||||
with Image.open(
|
with Image.open(
|
||||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||||
) as im:
|
) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
|
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
|
||||||
assert im.info["dpi"] == (dpi, dpi)
|
assert im.info["dpi"] == (dpi, dpi)
|
||||||
|
|
||||||
def test_save_float_dpi(self, tmp_path: Path) -> None:
|
def test_save_float_dpi(self, tmp_path: Path) -> None:
|
||||||
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:
|
||||||
dpi = (72.2, 72.2)
|
dpi = (72.2, 72.2)
|
||||||
im.save(outfile, dpi=dpi)
|
im.save(outfile, dpi=dpi)
|
||||||
|
@ -198,6 +215,7 @@ class TestFileTiff:
|
||||||
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
|
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
|
||||||
im.save(b, format="tiff", resolution=123.45)
|
im.save(b, format="tiff", resolution=123.45)
|
||||||
with Image.open(b) as im:
|
with Image.open(b) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.tag_v2[X_RESOLUTION] == 123.45
|
assert im.tag_v2[X_RESOLUTION] == 123.45
|
||||||
assert im.tag_v2[Y_RESOLUTION] == 123.45
|
assert im.tag_v2[Y_RESOLUTION] == 123.45
|
||||||
|
|
||||||
|
@ -213,19 +231,21 @@ class TestFileTiff:
|
||||||
TiffImagePlugin.PREFIXES.pop()
|
TiffImagePlugin.PREFIXES.pop()
|
||||||
|
|
||||||
def test_bad_exif(self) -> None:
|
def test_bad_exif(self) -> None:
|
||||||
with Image.open("Tests/images/hopper_bad_exif.jpg") as i:
|
with Image.open("Tests/images/hopper_bad_exif.jpg") as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
|
|
||||||
# Should not raise struct.error.
|
# Should not raise struct.error.
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
i._getexif()
|
im._getexif()
|
||||||
|
|
||||||
def test_save_rgba(self, tmp_path: Path) -> None:
|
def test_save_rgba(self, tmp_path: Path) -> None:
|
||||||
im = hopper("RGBA")
|
im = hopper("RGBA")
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
def test_save_unsupported_mode(self, tmp_path: Path) -> None:
|
def test_save_unsupported_mode(self, tmp_path: Path) -> None:
|
||||||
im = hopper("HSV")
|
im = hopper("HSV")
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
|
@ -307,11 +327,13 @@ class TestFileTiff:
|
||||||
)
|
)
|
||||||
def test_n_frames(self, path: str, n_frames: int) -> None:
|
def test_n_frames(self, path: str, n_frames: int) -> None:
|
||||||
with Image.open(path) as im:
|
with Image.open(path) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
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)
|
||||||
|
|
||||||
def test_eoferror(self) -> None:
|
def test_eoferror(self) -> None:
|
||||||
with Image.open("Tests/images/multipage-lastframe.tif") as im:
|
with Image.open("Tests/images/multipage-lastframe.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
# Test seeking past the last frame
|
# Test seeking past the last frame
|
||||||
|
@ -355,19 +377,24 @@ class TestFileTiff:
|
||||||
def test_frame_order(self) -> None:
|
def test_frame_order(self) -> None:
|
||||||
# A frame can't progress to itself after reading
|
# A frame can't progress to itself after reading
|
||||||
with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
|
with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
|
|
||||||
# A frame can't progress to a frame that has already been read
|
# A frame can't progress to a frame that has already been read
|
||||||
with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im:
|
with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
|
|
||||||
# Frames don't have to be in sequence
|
# Frames don't have to be in sequence
|
||||||
with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
|
with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.n_frames == 3
|
assert im.n_frames == 3
|
||||||
|
|
||||||
def test___str__(self) -> None:
|
def test___str__(self) -> None:
|
||||||
filename = "Tests/images/pil136.tiff"
|
filename = "Tests/images/pil136.tiff"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
ret = str(im.ifd)
|
ret = str(im.ifd)
|
||||||
|
|
||||||
|
@ -378,6 +405,8 @@ class TestFileTiff:
|
||||||
# Arrange
|
# Arrange
|
||||||
filename = "Tests/images/pil136.tiff"
|
filename = "Tests/images/pil136.tiff"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# v2 interface
|
# v2 interface
|
||||||
v2_tags = {
|
v2_tags = {
|
||||||
256: 55,
|
256: 55,
|
||||||
|
@ -417,6 +446,7 @@ class TestFileTiff:
|
||||||
def test__delitem__(self) -> None:
|
def test__delitem__(self) -> None:
|
||||||
filename = "Tests/images/pil136.tiff"
|
filename = "Tests/images/pil136.tiff"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
len_before = len(dict(im.ifd))
|
len_before = len(dict(im.ifd))
|
||||||
del im.ifd[256]
|
del im.ifd[256]
|
||||||
len_after = len(dict(im.ifd))
|
len_after = len(dict(im.ifd))
|
||||||
|
@ -449,6 +479,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
def test_ifd_tag_type(self) -> None:
|
def test_ifd_tag_type(self) -> None:
|
||||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert 0x8825 in im.tag_v2
|
assert 0x8825 in im.tag_v2
|
||||||
|
|
||||||
def test_exif(self, tmp_path: Path) -> None:
|
def test_exif(self, tmp_path: Path) -> None:
|
||||||
|
@ -485,14 +516,14 @@ class TestFileTiff:
|
||||||
assert gps[0] == b"\x03\x02\x00\x00"
|
assert gps[0] == b"\x03\x02\x00\x00"
|
||||||
assert gps[18] == "WGS-84"
|
assert gps[18] == "WGS-84"
|
||||||
|
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
check_exif(exif)
|
check_exif(exif)
|
||||||
|
|
||||||
im.save(outfile, exif=exif)
|
im.save(outfile, exif=exif)
|
||||||
|
|
||||||
outfile2 = str(tmp_path / "temp2.tif")
|
outfile2 = tmp_path / "temp2.tif"
|
||||||
with Image.open(outfile) as im:
|
with Image.open(outfile) as im:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
check_exif(exif)
|
check_exif(exif)
|
||||||
|
@ -504,7 +535,7 @@ class TestFileTiff:
|
||||||
check_exif(exif)
|
check_exif(exif)
|
||||||
|
|
||||||
def test_modify_exif(self, tmp_path: Path) -> None:
|
def test_modify_exif(self, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
exif[264] = 100
|
exif[264] = 100
|
||||||
|
@ -533,10 +564,11 @@ class TestFileTiff:
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "L"))
|
@pytest.mark.parametrize("mode", ("1", "L"))
|
||||||
def test_photometric(self, mode: str, tmp_path: Path) -> None:
|
def test_photometric(self, mode: str, tmp_path: Path) -> None:
|
||||||
filename = str(tmp_path / "temp.tif")
|
filename = tmp_path / "temp.tif"
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(filename, tiffinfo={262: 0})
|
im.save(filename, tiffinfo={262: 0})
|
||||||
with Image.open(filename) as reloaded:
|
with Image.open(filename) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[262] == 0
|
assert reloaded.tag_v2[262] == 0
|
||||||
assert_image_equal(im, reloaded)
|
assert_image_equal(im, reloaded)
|
||||||
|
|
||||||
|
@ -612,9 +644,11 @@ class TestFileTiff:
|
||||||
|
|
||||||
def test_with_underscores(self, tmp_path: Path) -> None:
|
def test_with_underscores(self, tmp_path: Path) -> None:
|
||||||
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
|
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
|
||||||
filename = str(tmp_path / "temp.tif")
|
filename = tmp_path / "temp.tif"
|
||||||
hopper("RGB").save(filename, "TIFF", **kwargs)
|
hopper("RGB").save(filename, "TIFF", **kwargs)
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# legacy interface
|
# legacy interface
|
||||||
assert im.tag[X_RESOLUTION][0][0] == 72
|
assert im.tag[X_RESOLUTION][0][0] == 72
|
||||||
assert im.tag[Y_RESOLUTION][0][0] == 36
|
assert im.tag[Y_RESOLUTION][0][0] == 36
|
||||||
|
@ -630,14 +664,14 @@ class TestFileTiff:
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
assert im.getpixel((0, 0)) == pixel_value
|
assert im.getpixel((0, 0)) == pixel_value
|
||||||
|
|
||||||
tmpfile = str(tmp_path / "temp.tif")
|
tmpfile = tmp_path / "temp.tif"
|
||||||
im.save(tmpfile)
|
im.save(tmpfile)
|
||||||
|
|
||||||
assert_image_equal_tofile(im, tmpfile)
|
assert_image_equal_tofile(im, tmpfile)
|
||||||
|
|
||||||
def test_iptc(self, tmp_path: Path) -> None:
|
def test_iptc(self, tmp_path: Path) -> None:
|
||||||
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
|
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
|
||||||
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:
|
||||||
im.load()
|
im.load()
|
||||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
@ -652,7 +686,7 @@ class TestFileTiff:
|
||||||
assert 33723 not in im.tag_v2
|
assert 33723 not in im.tag_v2
|
||||||
|
|
||||||
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
im = hopper()
|
im = hopper()
|
||||||
im.save(outfile, tiffinfo={278: 256})
|
im.save(outfile, tiffinfo={278: 256})
|
||||||
|
|
||||||
|
@ -701,9 +735,10 @@ class TestFileTiff:
|
||||||
def test_planar_configuration_save(self, tmp_path: Path) -> None:
|
def test_planar_configuration_save(self, tmp_path: Path) -> None:
|
||||||
infile = "Tests/images/tiff_tiled_planar_raw.tif"
|
infile = "Tests/images/tiff_tiled_planar_raw.tif"
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im._planar_configuration == 2
|
assert im._planar_configuration == 2
|
||||||
|
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
with Image.open(outfile) as reloaded:
|
with Image.open(outfile) as reloaded:
|
||||||
|
@ -718,7 +753,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||||
def test_palette(self, mode: str, tmp_path: Path) -> None:
|
def test_palette(self, mode: str, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
|
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
@ -733,6 +768,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
mp.seek(0, os.SEEK_SET)
|
mp.seek(0, os.SEEK_SET)
|
||||||
with Image.open(mp) as im:
|
with Image.open(mp) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.n_frames == 3
|
assert im.n_frames == 3
|
||||||
|
|
||||||
# Test appending images
|
# Test appending images
|
||||||
|
@ -743,6 +779,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
mp.seek(0, os.SEEK_SET)
|
mp.seek(0, os.SEEK_SET)
|
||||||
with Image.open(mp) as reread:
|
with Image.open(mp) as reread:
|
||||||
|
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
# Test appending using a generator
|
# Test appending using a generator
|
||||||
|
@ -754,6 +791,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
mp.seek(0, os.SEEK_SET)
|
mp.seek(0, os.SEEK_SET)
|
||||||
with Image.open(mp) as reread:
|
with Image.open(mp) as reread:
|
||||||
|
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
def test_fixoffsets(self) -> None:
|
def test_fixoffsets(self) -> None:
|
||||||
|
@ -812,7 +850,7 @@ class TestFileTiff:
|
||||||
im.info["icc_profile"] = "Dummy value"
|
im.info["icc_profile"] = "Dummy value"
|
||||||
|
|
||||||
# Try save-load round trip to make sure both handle icc_profile.
|
# Try save-load round trip to make sure both handle icc_profile.
|
||||||
tmpfile = str(tmp_path / "temp.tif")
|
tmpfile = tmp_path / "temp.tif"
|
||||||
im.save(tmpfile, "TIFF", compression="raw")
|
im.save(tmpfile, "TIFF", compression="raw")
|
||||||
with Image.open(tmpfile) as reloaded:
|
with Image.open(tmpfile) as reloaded:
|
||||||
assert b"Dummy value" == reloaded.info["icc_profile"]
|
assert b"Dummy value" == reloaded.info["icc_profile"]
|
||||||
|
@ -821,7 +859,7 @@ class TestFileTiff:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
assert "icc_profile" not in im.info
|
assert "icc_profile" not in im.info
|
||||||
|
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
icc_profile = b"Dummy value"
|
icc_profile = b"Dummy value"
|
||||||
im.save(outfile, icc_profile=icc_profile)
|
im.save(outfile, icc_profile=icc_profile)
|
||||||
|
|
||||||
|
@ -832,11 +870,11 @@ class TestFileTiff:
|
||||||
with Image.open("Tests/images/hopper.bmp") as im:
|
with Image.open("Tests/images/hopper.bmp") as im:
|
||||||
assert im.info["compression"] == 0
|
assert im.info["compression"] == 0
|
||||||
|
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
def test_discard_icc_profile(self, tmp_path: Path) -> None:
|
def test_discard_icc_profile(self, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = tmp_path / "temp.tif"
|
||||||
|
|
||||||
with Image.open("Tests/images/icc_profile.png") as im:
|
with Image.open("Tests/images/icc_profile.png") as im:
|
||||||
assert "icc_profile" in im.info
|
assert "icc_profile" in im.info
|
||||||
|
@ -864,6 +902,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
def test_get_photoshop_blocks(self) -> None:
|
def test_get_photoshop_blocks(self) -> None:
|
||||||
with Image.open("Tests/images/lab.tif") as im:
|
with Image.open("Tests/images/lab.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert list(im.get_photoshop_blocks().keys()) == [
|
assert list(im.get_photoshop_blocks().keys()) == [
|
||||||
1061,
|
1061,
|
||||||
1002,
|
1002,
|
||||||
|
@ -889,7 +928,7 @@ class TestFileTiff:
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_tiff_chunks(self, tmp_path: Path) -> None:
|
def test_tiff_chunks(self, tmp_path: Path) -> None:
|
||||||
tmpfile = str(tmp_path / "temp.tif")
|
tmpfile = tmp_path / "temp.tif"
|
||||||
|
|
||||||
im = hopper()
|
im = hopper()
|
||||||
with open(tmpfile, "wb") as fp:
|
with open(tmpfile, "wb") as fp:
|
||||||
|
@ -911,7 +950,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
def test_close_on_load_exclusive(self, tmp_path: Path) -> None:
|
def test_close_on_load_exclusive(self, tmp_path: Path) -> None:
|
||||||
# similar to test_fd_leak, but runs on unixlike os
|
# similar to test_fd_leak, but runs on unixlike os
|
||||||
tmpfile = str(tmp_path / "temp.tif")
|
tmpfile = tmp_path / "temp.tif"
|
||||||
|
|
||||||
with Image.open("Tests/images/uint16_1_4660.tif") as im:
|
with Image.open("Tests/images/uint16_1_4660.tif") as im:
|
||||||
im.save(tmpfile)
|
im.save(tmpfile)
|
||||||
|
@ -923,7 +962,7 @@ class TestFileTiff:
|
||||||
assert fp.closed
|
assert fp.closed
|
||||||
|
|
||||||
def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None:
|
def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None:
|
||||||
tmpfile = str(tmp_path / "temp.tif")
|
tmpfile = tmp_path / "temp.tif"
|
||||||
|
|
||||||
with Image.open("Tests/images/uint16_1_4660.tif") as im:
|
with Image.open("Tests/images/uint16_1_4660.tif") as im:
|
||||||
im.save(tmpfile)
|
im.save(tmpfile)
|
||||||
|
@ -950,7 +989,7 @@ class TestFileTiff:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
@pytest.mark.timeout(6)
|
@timeout_unless_slower_valgrind(6)
|
||||||
@pytest.mark.filterwarnings("ignore:Truncated File Read")
|
@pytest.mark.filterwarnings("ignore:Truncated File Read")
|
||||||
def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
with Image.open("Tests/images/timeout-6646305047838720") as im:
|
with Image.open("Tests/images/timeout-6646305047838720") as im:
|
||||||
|
@ -963,7 +1002,7 @@ class TestFileTiff:
|
||||||
"Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif",
|
"Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.timeout(2)
|
@timeout_unless_slower_valgrind(2)
|
||||||
def test_oom(self, test_file: str) -> None:
|
def test_oom(self, test_file: str) -> None:
|
||||||
with pytest.raises(UnidentifiedImageError):
|
with pytest.raises(UnidentifiedImageError):
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
|
@ -974,7 +1013,7 @@ class TestFileTiff:
|
||||||
@pytest.mark.skipif(not is_win32(), reason="Windows only")
|
@pytest.mark.skipif(not is_win32(), reason="Windows only")
|
||||||
class TestFileTiffW32:
|
class TestFileTiffW32:
|
||||||
def test_fd_leak(self, tmp_path: Path) -> None:
|
def test_fd_leak(self, tmp_path: Path) -> None:
|
||||||
tmpfile = str(tmp_path / "temp.tif")
|
tmpfile = tmp_path / "temp.tif"
|
||||||
|
|
||||||
# this is an mmaped file.
|
# this is an mmaped file.
|
||||||
with Image.open("Tests/images/uint16_1_4660.tif") as im:
|
with Image.open("Tests/images/uint16_1_4660.tif") as im:
|
||||||
|
|
|
@ -56,11 +56,12 @@ def test_rt_metadata(tmp_path: Path) -> None:
|
||||||
|
|
||||||
info[ImageDescription] = text_data
|
info[ImageDescription] = text_data
|
||||||
|
|
||||||
f = str(tmp_path / "temp.tif")
|
f = tmp_path / "temp.tif"
|
||||||
|
|
||||||
img.save(f, tiffinfo=info)
|
img.save(f, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(f) as loaded:
|
with Image.open(f) as loaded:
|
||||||
|
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
|
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
|
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||||
|
|
||||||
|
@ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None:
|
||||||
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
|
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
|
||||||
img.save(f, tiffinfo=info)
|
img.save(f, tiffinfo=info)
|
||||||
with Image.open(f) as loaded:
|
with Image.open(f) as loaded:
|
||||||
|
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||||
|
|
||||||
|
|
||||||
def test_read_metadata() -> None:
|
def test_read_metadata() -> None:
|
||||||
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)
|
||||||
assert {
|
assert {
|
||||||
"YResolution": IFDRational(4294967295, 113653537),
|
"YResolution": IFDRational(4294967295, 113653537),
|
||||||
"PlanarConfiguration": 1,
|
"PlanarConfiguration": 1,
|
||||||
|
@ -128,13 +131,15 @@ def test_read_metadata() -> None:
|
||||||
def test_write_metadata(tmp_path: Path) -> None:
|
def test_write_metadata(tmp_path: Path) -> None:
|
||||||
"""Test metadata writing through the python code"""
|
"""Test metadata writing through the python code"""
|
||||||
with Image.open("Tests/images/hopper.tif") as img:
|
with Image.open("Tests/images/hopper.tif") as img:
|
||||||
f = str(tmp_path / "temp.tiff")
|
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||||
|
f = tmp_path / "temp.tiff"
|
||||||
del img.tag[278]
|
del img.tag[278]
|
||||||
img.save(f, tiffinfo=img.tag)
|
img.save(f, tiffinfo=img.tag)
|
||||||
|
|
||||||
original = img.tag_v2.named()
|
original = img.tag_v2.named()
|
||||||
|
|
||||||
with Image.open(f) as loaded:
|
with Image.open(f) as loaded:
|
||||||
|
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||||
reloaded = loaded.tag_v2.named()
|
reloaded = loaded.tag_v2.named()
|
||||||
|
|
||||||
ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"]
|
ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"]
|
||||||
|
@ -163,8 +168,9 @@ def test_write_metadata(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
with Image.open("Tests/images/hopper.tif") as im:
|
with Image.open("Tests/images/hopper.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
info = im.tag_v2
|
info = im.tag_v2
|
||||||
del info[278]
|
del info[278]
|
||||||
|
|
||||||
|
@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
||||||
im.save(out, tiffinfo=info)
|
im.save(out, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
|
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
|
||||||
|
|
||||||
|
|
||||||
|
@ -210,7 +217,7 @@ def test_no_duplicate_50741_tag() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_iptc(tmp_path: Path) -> None:
|
def test_iptc(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
with Image.open("Tests/images/hopper.Lab.tif") as im:
|
with Image.open("Tests/images/hopper.Lab.tif") as im:
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
||||||
|
@ -227,10 +234,11 @@ def test_writing_other_types_to_ascii(
|
||||||
info[271] = value
|
info[271] = value
|
||||||
|
|
||||||
im = hopper()
|
im = hopper()
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info)
|
im.save(out, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[271] == expected
|
assert reloaded.tag_v2[271] == expected
|
||||||
|
|
||||||
|
|
||||||
|
@ -244,10 +252,11 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path)
|
||||||
|
|
||||||
info[700] = value
|
info[700] = value
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info)
|
im.save(out, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[700] == b"\x01"
|
assert reloaded.tag_v2[700] == b"\x01"
|
||||||
|
|
||||||
|
|
||||||
|
@ -263,10 +272,11 @@ def test_writing_other_types_to_undefined(
|
||||||
|
|
||||||
info[33723] = value
|
info[33723] = value
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info)
|
im.save(out, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[33723] == b"1"
|
assert reloaded.tag_v2[33723] == b"1"
|
||||||
|
|
||||||
|
|
||||||
|
@ -296,7 +306,7 @@ def test_empty_metadata() -> None:
|
||||||
|
|
||||||
def test_iccprofile(tmp_path: Path) -> None:
|
def test_iccprofile(tmp_path: Path) -> None:
|
||||||
# https://github.com/python-pillow/Pillow/issues/1462
|
# https://github.com/python-pillow/Pillow/issues/1462
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
|
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
||||||
|
@ -311,19 +321,20 @@ def test_iccprofile_binary() -> None:
|
||||||
# but probably won't be able to save it.
|
# but probably won't be able to save it.
|
||||||
|
|
||||||
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
|
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.tag_v2.tagtype[34675] == 1
|
assert im.tag_v2.tagtype[34675] == 1
|
||||||
assert im.info["icc_profile"]
|
assert im.info["icc_profile"]
|
||||||
|
|
||||||
|
|
||||||
def test_iccprofile_save_png(tmp_path: Path) -> None:
|
def test_iccprofile_save_png(tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
|
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
|
||||||
outfile = str(tmp_path / "temp.png")
|
outfile = tmp_path / "temp.png"
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
|
|
||||||
def test_iccprofile_binary_save_png(tmp_path: Path) -> None:
|
def test_iccprofile_binary_save_png(tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
|
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
|
||||||
outfile = str(tmp_path / "temp.png")
|
outfile = tmp_path / "temp.png"
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
|
|
||||||
|
@ -332,10 +343,11 @@ def test_exif_div_zero(tmp_path: Path) -> None:
|
||||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
info[41988] = TiffImagePlugin.IFDRational(0, 0)
|
info[41988] = TiffImagePlugin.IFDRational(0, 0)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert 0 == reloaded.tag_v2[41988].numerator
|
assert 0 == reloaded.tag_v2[41988].numerator
|
||||||
assert 0 == reloaded.tag_v2[41988].denominator
|
assert 0 == reloaded.tag_v2[41988].denominator
|
||||||
|
|
||||||
|
@ -351,10 +363,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
|
||||||
|
|
||||||
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
|
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert max_long == reloaded.tag_v2[41493].numerator
|
assert max_long == reloaded.tag_v2[41493].numerator
|
||||||
assert 1 == reloaded.tag_v2[41493].denominator
|
assert 1 == reloaded.tag_v2[41493].denominator
|
||||||
|
|
||||||
|
@ -363,10 +376,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
|
||||||
|
|
||||||
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
|
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert max_long == reloaded.tag_v2[41493].numerator
|
assert max_long == reloaded.tag_v2[41493].numerator
|
||||||
assert 1 == reloaded.tag_v2[41493].denominator
|
assert 1 == reloaded.tag_v2[41493].denominator
|
||||||
|
|
||||||
|
@ -381,10 +395,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
||||||
|
|
||||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert numerator == reloaded.tag_v2[37380].numerator
|
assert numerator == reloaded.tag_v2[37380].numerator
|
||||||
assert denominator == reloaded.tag_v2[37380].denominator
|
assert denominator == reloaded.tag_v2[37380].denominator
|
||||||
|
|
||||||
|
@ -393,10 +408,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
||||||
|
|
||||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert numerator == reloaded.tag_v2[37380].numerator
|
assert numerator == reloaded.tag_v2[37380].numerator
|
||||||
assert denominator == reloaded.tag_v2[37380].denominator
|
assert denominator == reloaded.tag_v2[37380].denominator
|
||||||
|
|
||||||
|
@ -406,10 +422,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
||||||
|
|
||||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
|
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
|
||||||
assert -1 == reloaded.tag_v2[37380].denominator
|
assert -1 == reloaded.tag_v2[37380].denominator
|
||||||
|
|
||||||
|
@ -420,10 +437,11 @@ def test_ifd_signed_long(tmp_path: Path) -> None:
|
||||||
|
|
||||||
info[37000] = -60000
|
info[37000] = -60000
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[37000] == -60000
|
assert reloaded.tag_v2[37000] == -60000
|
||||||
|
|
||||||
|
|
||||||
|
@ -444,11 +462,13 @@ def test_empty_values() -> None:
|
||||||
|
|
||||||
def test_photoshop_info(tmp_path: Path) -> None:
|
def test_photoshop_info(tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/issue_2278.tif") as im:
|
with Image.open("Tests/images/issue_2278.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert len(im.tag_v2[34377]) == 70
|
assert len(im.tag_v2[34377]) == 70
|
||||||
assert isinstance(im.tag_v2[34377], bytes)
|
assert isinstance(im.tag_v2[34377], bytes)
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out)
|
im.save(out)
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert len(reloaded.tag_v2[34377]) == 70
|
assert len(reloaded.tag_v2[34377]) == 70
|
||||||
assert isinstance(reloaded.tag_v2[34377], bytes)
|
assert isinstance(reloaded.tag_v2[34377], bytes)
|
||||||
|
|
||||||
|
@ -480,7 +500,7 @@ def test_tag_group_data() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_empty_subifd(tmp_path: Path) -> None:
|
def test_empty_subifd(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.jpg")
|
out = tmp_path / "temp.jpg"
|
||||||
|
|
||||||
im = hopper()
|
im = hopper()
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
|
|
|
@ -42,7 +42,7 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
|
||||||
Does it have the bits we expect?
|
Does it have the bits we expect?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
# temp_file = "temp.webp"
|
# temp_file = "temp.webp"
|
||||||
|
|
||||||
pil_image = hopper("RGBA")
|
pil_image = hopper("RGBA")
|
||||||
|
@ -71,7 +71,7 @@ def test_write_rgba(tmp_path: Path) -> None:
|
||||||
Does it have the bits we expect?
|
Does it have the bits we expect?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
|
|
||||||
pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20))
|
pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20))
|
||||||
pil_image.save(temp_file)
|
pil_image.save(temp_file)
|
||||||
|
@ -104,7 +104,7 @@ def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None:
|
||||||
half_transparent_image.putalpha(new_alpha)
|
half_transparent_image.putalpha(new_alpha)
|
||||||
|
|
||||||
# save with transparent area preserved
|
# save with transparent area preserved
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
half_transparent_image.save(temp_file, exact=True, lossless=True)
|
half_transparent_image.save(temp_file, exact=True, lossless=True)
|
||||||
|
|
||||||
with Image.open(temp_file) as reloaded:
|
with Image.open(temp_file) as reloaded:
|
||||||
|
@ -123,7 +123,7 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
|
||||||
should work, and be similar to the original file.
|
should work, and be similar to the original file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
file_path = "Tests/images/transparent.gif"
|
file_path = "Tests/images/transparent.gif"
|
||||||
with Image.open(file_path) as im:
|
with Image.open(file_path) as im:
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
@ -142,10 +142,10 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
|
||||||
|
|
||||||
def test_alpha_quality(tmp_path: Path) -> None:
|
def test_alpha_quality(tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/transparent.png") as im:
|
with Image.open("Tests/images/transparent.png") as im:
|
||||||
out = str(tmp_path / "temp.webp")
|
out = tmp_path / "temp.webp"
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
||||||
out_quality = str(tmp_path / "quality.webp")
|
out_quality = tmp_path / "quality.webp"
|
||||||
im.save(out_quality, alpha_quality=50)
|
im.save(out_quality, alpha_quality=50)
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
with Image.open(out_quality) as reloaded_quality:
|
with Image.open(out_quality) as reloaded_quality:
|
||||||
|
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
from PIL import Image, features
|
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -22,10 +22,12 @@ def test_n_frames() -> None:
|
||||||
"""Ensure that WebP format sets n_frames and is_animated attributes correctly."""
|
"""Ensure that WebP format sets n_frames and is_animated attributes correctly."""
|
||||||
|
|
||||||
with Image.open("Tests/images/hopper.webp") as im:
|
with Image.open("Tests/images/hopper.webp") as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
with Image.open("Tests/images/iss634.webp") as im:
|
with Image.open("Tests/images/iss634.webp") as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 42
|
assert im.n_frames == 42
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
@ -37,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with Image.open("Tests/images/iss634.gif") as orig:
|
with Image.open("Tests/images/iss634.gif") as orig:
|
||||||
|
assert isinstance(orig, GifImagePlugin.GifImageFile)
|
||||||
assert orig.n_frames > 1
|
assert orig.n_frames > 1
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
orig.save(temp_file, save_all=True)
|
orig.save(temp_file, save_all=True)
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == orig.n_frames
|
assert im.n_frames == orig.n_frames
|
||||||
|
|
||||||
# Compare first and last frames to the original animated GIF
|
# Compare first and last frames to the original animated GIF
|
||||||
|
@ -67,8 +71,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
||||||
are visually similar to the originals.
|
are visually similar to the originals.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def check(temp_file: str) -> None:
|
def check(temp_file: Path) -> None:
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
|
|
||||||
# Compare first frame to original
|
# Compare first frame to original
|
||||||
|
@ -87,7 +92,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
||||||
|
|
||||||
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
||||||
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
||||||
temp_file1 = str(tmp_path / "temp.webp")
|
temp_file1 = tmp_path / "temp.webp"
|
||||||
frame1.copy().save(
|
frame1.copy().save(
|
||||||
temp_file1, save_all=True, append_images=[frame2], lossless=True
|
temp_file1, save_all=True, append_images=[frame2], lossless=True
|
||||||
)
|
)
|
||||||
|
@ -99,7 +104,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
||||||
) -> Generator[Image.Image, None, None]:
|
) -> Generator[Image.Image, None, None]:
|
||||||
yield from ims
|
yield from ims
|
||||||
|
|
||||||
temp_file2 = str(tmp_path / "temp_generator.webp")
|
temp_file2 = tmp_path / "temp_generator.webp"
|
||||||
frame1.copy().save(
|
frame1.copy().save(
|
||||||
temp_file2,
|
temp_file2,
|
||||||
save_all=True,
|
save_all=True,
|
||||||
|
@ -116,7 +121,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
durations = [0, 10, 20, 30, 40]
|
durations = [0, 10, 20, 30, 40]
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
||||||
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
||||||
frame1.save(
|
frame1.save(
|
||||||
|
@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 5
|
assert im.n_frames == 5
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
@ -141,7 +147,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_float_duration(tmp_path: Path) -> None:
|
def test_float_duration(tmp_path: Path) -> None:
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
with Image.open("Tests/images/iss634.apng") as im:
|
with Image.open("Tests/images/iss634.apng") as im:
|
||||||
assert im.info["duration"] == 70.0
|
assert im.info["duration"] == 70.0
|
||||||
|
|
||||||
|
@ -159,7 +165,7 @@ def test_seeking(tmp_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dur = 33
|
dur = 33
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
||||||
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
||||||
frame1.save(
|
frame1.save(
|
||||||
|
@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 5
|
assert im.n_frames == 5
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
@ -196,10 +203,10 @@ def test_alpha_quality(tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/transparent.png") as im:
|
with Image.open("Tests/images/transparent.png") as im:
|
||||||
first_frame = Image.new("L", im.size)
|
first_frame = Image.new("L", im.size)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.webp")
|
out = tmp_path / "temp.webp"
|
||||||
first_frame.save(out, save_all=True, append_images=[im])
|
first_frame.save(out, save_all=True, append_images=[im])
|
||||||
|
|
||||||
out_quality = str(tmp_path / "quality.webp")
|
out_quality = tmp_path / "quality.webp"
|
||||||
first_frame.save(
|
first_frame.save(
|
||||||
out_quality, save_all=True, append_images=[im], alpha_quality=50
|
out_quality, save_all=True, append_images=[im], alpha_quality=50
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ RGB_MODE = "RGB"
|
||||||
|
|
||||||
|
|
||||||
def test_write_lossless_rgb(tmp_path: Path) -> None:
|
def test_write_lossless_rgb(tmp_path: Path) -> None:
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
|
|
||||||
hopper(RGB_MODE).save(temp_file, lossless=True)
|
hopper(RGB_MODE).save(temp_file, lossless=True)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, WebPImagePlugin
|
||||||
|
|
||||||
from .helper import mark_if_feature_version, skip_unless_feature
|
from .helper import mark_if_feature_version, skip_unless_feature
|
||||||
|
|
||||||
|
@ -110,6 +110,7 @@ def test_read_no_exif() -> None:
|
||||||
|
|
||||||
test_buffer.seek(0)
|
test_buffer.seek(0)
|
||||||
with Image.open(test_buffer) as webp_image:
|
with Image.open(test_buffer) as webp_image:
|
||||||
|
assert isinstance(webp_image, WebPImagePlugin.WebPImageFile)
|
||||||
assert not webp_image._getexif()
|
assert not webp_image._getexif()
|
||||||
|
|
||||||
|
|
||||||
|
@ -146,7 +147,7 @@ def test_write_animated_metadata(tmp_path: Path) -> None:
|
||||||
exif_data = b"<exif_data>"
|
exif_data = b"<exif_data>"
|
||||||
xmp_data = b"<xmp_data>"
|
xmp_data = b"<xmp_data>"
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = tmp_path / "temp.webp"
|
||||||
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
with Image.open("Tests/images/anim_frame1.webp") as frame1:
|
||||||
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
with Image.open("Tests/images/anim_frame2.webp") as frame2:
|
||||||
frame1.save(
|
frame1.save(
|
||||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFile, WmfImagePlugin
|
from PIL import Image, ImageFile, WmfImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_similar_tofile, hopper
|
from .helper import assert_image_equal_tofile, assert_image_similar_tofile, hopper
|
||||||
|
|
||||||
|
|
||||||
def test_load_raw() -> None:
|
def test_load_raw() -> None:
|
||||||
|
@ -44,6 +44,15 @@ def test_load_zero_inch() -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_render() -> None:
|
||||||
|
with open("Tests/images/drawing.emf", "rb") as fp:
|
||||||
|
data = fp.read()
|
||||||
|
b = BytesIO(data[:808] + b"\x00" + data[809:])
|
||||||
|
with Image.open(b) as im:
|
||||||
|
if hasattr(Image.core, "drawwmf"):
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/drawing.emf")
|
||||||
|
|
||||||
|
|
||||||
def test_register_handler(tmp_path: Path) -> None:
|
def test_register_handler(tmp_path: Path) -> None:
|
||||||
class TestHandler(ImageFile.StubHandler):
|
class TestHandler(ImageFile.StubHandler):
|
||||||
methodCalled = False
|
methodCalled = False
|
||||||
|
@ -59,7 +68,7 @@ def test_register_handler(tmp_path: Path) -> None:
|
||||||
WmfImagePlugin.register_handler(handler)
|
WmfImagePlugin.register_handler(handler)
|
||||||
|
|
||||||
im = hopper()
|
im = hopper()
|
||||||
tmpfile = str(tmp_path / "temp.wmf")
|
tmpfile = tmp_path / "temp.wmf"
|
||||||
im.save(tmpfile)
|
im.save(tmpfile)
|
||||||
assert handler.methodCalled
|
assert handler.methodCalled
|
||||||
|
|
||||||
|
@ -80,6 +89,7 @@ def test_load_float_dpi() -> None:
|
||||||
|
|
||||||
def test_load_set_dpi() -> None:
|
def test_load_set_dpi() -> None:
|
||||||
with Image.open("Tests/images/drawing.wmf") as im:
|
with Image.open("Tests/images/drawing.wmf") as im:
|
||||||
|
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||||
assert im.size == (82, 82)
|
assert im.size == (82, 82)
|
||||||
|
|
||||||
if hasattr(Image.core, "drawwmf"):
|
if hasattr(Image.core, "drawwmf"):
|
||||||
|
@ -88,11 +98,27 @@ def test_load_set_dpi() -> None:
|
||||||
|
|
||||||
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1)
|
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1)
|
||||||
|
|
||||||
|
with Image.open("Tests/images/drawing.emf") as im:
|
||||||
|
assert im.size == (1625, 1625)
|
||||||
|
|
||||||
|
if not hasattr(Image.core, "drawwmf"):
|
||||||
|
return
|
||||||
|
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||||
|
im.load(im.info["dpi"])
|
||||||
|
assert im.size == (1625, 1625)
|
||||||
|
|
||||||
|
with Image.open("Tests/images/drawing.emf") as im:
|
||||||
|
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||||
|
im.load((72, 144))
|
||||||
|
assert im.size == (82, 164)
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref_72_144.png")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
|
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
|
||||||
def test_save(ext: str, tmp_path: Path) -> None:
|
def test_save(ext: str, tmp_path: Path) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
||||||
tmpfile = str(tmp_path / ("temp" + ext))
|
tmpfile = tmp_path / ("temp" + ext)
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.save(tmpfile)
|
im.save(tmpfile)
|
||||||
|
|
|
@ -73,7 +73,7 @@ def test_invalid_file() -> None:
|
||||||
|
|
||||||
def test_save_wrong_mode(tmp_path: Path) -> None:
|
def test_save_wrong_mode(tmp_path: Path) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
out = str(tmp_path / "temp.xbm")
|
out = tmp_path / "temp.xbm"
|
||||||
|
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
@ -81,7 +81,7 @@ def test_save_wrong_mode(tmp_path: Path) -> None:
|
||||||
|
|
||||||
def test_hotspot(tmp_path: Path) -> None:
|
def test_hotspot(tmp_path: Path) -> None:
|
||||||
im = hopper("1")
|
im = hopper("1")
|
||||||
out = str(tmp_path / "temp.xbm")
|
out = tmp_path / "temp.xbm"
|
||||||
|
|
||||||
hotspot = (0, 7)
|
hotspot = (0, 7)
|
||||||
im.save(out, hotspot=hotspot)
|
im.save(out, hotspot=hotspot)
|
||||||
|
|
|
@ -30,6 +30,7 @@ def test_invalid_file() -> None:
|
||||||
def test_load_read() -> None:
|
def test_load_read() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, XpmImagePlugin.XpmImageFile)
|
||||||
dummy_bytes = 1
|
dummy_bytes = 1
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
|
|
@ -34,6 +34,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
|
||||||
|
@ -175,6 +176,13 @@ class TestImage:
|
||||||
with Image.open(io.StringIO()): # type: ignore[arg-type]
|
with Image.open(io.StringIO()): # type: ignore[arg-type]
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def test_string(self, tmp_path: Path) -> None:
|
||||||
|
out = str(tmp_path / "temp.png")
|
||||||
|
im = hopper()
|
||||||
|
im.save(out)
|
||||||
|
with Image.open(out) as reloaded:
|
||||||
|
assert_image_equal(im, reloaded)
|
||||||
|
|
||||||
def test_pathlib(self, tmp_path: Path) -> None:
|
def test_pathlib(self, tmp_path: Path) -> None:
|
||||||
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
|
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
|
@ -187,14 +195,13 @@ class TestImage:
|
||||||
for ext in (".jpg", ".jp2"):
|
for ext in (".jpg", ".jp2"):
|
||||||
if ext == ".jp2" and not features.check_codec("jpg_2000"):
|
if ext == ".jp2" and not features.check_codec("jpg_2000"):
|
||||||
pytest.skip("jpg_2000 not available")
|
pytest.skip("jpg_2000 not available")
|
||||||
temp_file = str(tmp_path / ("temp." + ext))
|
im.save(tmp_path / ("temp." + ext))
|
||||||
im.save(Path(temp_file))
|
|
||||||
|
|
||||||
def test_fp_name(self, tmp_path: Path) -> None:
|
def test_fp_name(self, tmp_path: Path) -> None:
|
||||||
temp_file = str(tmp_path / "temp.jpg")
|
temp_file = tmp_path / "temp.jpg"
|
||||||
|
|
||||||
class FP(io.BytesIO):
|
class FP(io.BytesIO):
|
||||||
name: str
|
name: Path
|
||||||
|
|
||||||
if sys.version_info >= (3, 12):
|
if sys.version_info >= (3, 12):
|
||||||
from collections.abc import Buffer
|
from collections.abc import Buffer
|
||||||
|
@ -224,10 +231,10 @@ class TestImage:
|
||||||
assert_image_similar(im, reloaded, 20)
|
assert_image_similar(im, reloaded, 20)
|
||||||
|
|
||||||
def test_unknown_extension(self, tmp_path: Path) -> None:
|
def test_unknown_extension(self, tmp_path: Path) -> None:
|
||||||
im = hopper()
|
temp_file = tmp_path / "temp.unknown"
|
||||||
temp_file = str(tmp_path / "temp.unknown")
|
with hopper() as im:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
||||||
def test_internals(self) -> None:
|
def test_internals(self) -> None:
|
||||||
im = Image.new("L", (100, 100))
|
im = Image.new("L", (100, 100))
|
||||||
|
@ -245,13 +252,22 @@ class TestImage:
|
||||||
reason="Test requires opening an mmaped file for writing",
|
reason="Test requires opening an mmaped file for writing",
|
||||||
)
|
)
|
||||||
def test_readonly_save(self, tmp_path: Path) -> None:
|
def test_readonly_save(self, tmp_path: Path) -> None:
|
||||||
temp_file = str(tmp_path / "temp.bmp")
|
temp_file = tmp_path / "temp.bmp"
|
||||||
shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
|
shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
|
||||||
|
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
assert im.readonly
|
assert im.readonly
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
||||||
|
def test_save_without_changing_readonly(self, tmp_path: Path) -> None:
|
||||||
|
temp_file = tmp_path / "temp.bmp"
|
||||||
|
|
||||||
|
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
|
||||||
|
assert im.readonly
|
||||||
|
|
||||||
|
im.save(temp_file)
|
||||||
|
assert im.readonly
|
||||||
|
|
||||||
def test_dump(self, tmp_path: Path) -> None:
|
def test_dump(self, tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (10, 10))
|
im = Image.new("L", (10, 10))
|
||||||
im._dump(str(tmp_path / "temp_L.ppm"))
|
im._dump(str(tmp_path / "temp_L.ppm"))
|
||||||
|
@ -557,10 +573,7 @@ class TestImage:
|
||||||
i = Image.new("RGB", [1, 1])
|
i = Image.new("RGB", [1, 1])
|
||||||
assert isinstance(i.size, tuple)
|
assert isinstance(i.size, tuple)
|
||||||
|
|
||||||
@pytest.mark.timeout(0.75)
|
@timeout_unless_slower_valgrind(0.75)
|
||||||
@pytest.mark.skipif(
|
|
||||||
"PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower"
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
|
@pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
|
||||||
def test_empty_image(self, size: tuple[int, int]) -> None:
|
def test_empty_image(self, size: tuple[int, int]) -> None:
|
||||||
Image.new("RGB", size)
|
Image.new("RGB", size)
|
||||||
|
@ -658,6 +671,7 @@ class TestImage:
|
||||||
im_remapped = im.remap_palette(list(range(256)))
|
im_remapped = im.remap_palette(list(range(256)))
|
||||||
assert_image_equal(im, im_remapped)
|
assert_image_equal(im, im_remapped)
|
||||||
assert im.palette is not None
|
assert im.palette is not None
|
||||||
|
assert im_remapped.palette is not None
|
||||||
assert im.palette.palette == im_remapped.palette.palette
|
assert im.palette.palette == im_remapped.palette.palette
|
||||||
|
|
||||||
# Test illegal image mode
|
# Test illegal image mode
|
||||||
|
@ -728,7 +742,7 @@ class TestImage:
|
||||||
# https://github.com/python-pillow/Pillow/issues/835
|
# https://github.com/python-pillow/Pillow/issues/835
|
||||||
# Arrange
|
# Arrange
|
||||||
test_file = "Tests/images/hopper.png"
|
test_file = "Tests/images/hopper.png"
|
||||||
temp_file = str(tmp_path / "temp.jpg")
|
temp_file = tmp_path / "temp.jpg"
|
||||||
|
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
@ -738,7 +752,7 @@ class TestImage:
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
||||||
def test_no_new_file_on_error(self, tmp_path: Path) -> None:
|
def test_no_new_file_on_error(self, tmp_path: Path) -> None:
|
||||||
temp_file = str(tmp_path / "temp.jpg")
|
temp_file = tmp_path / "temp.jpg"
|
||||||
|
|
||||||
im = Image.new("RGB", (0, 0))
|
im = Image.new("RGB", (0, 0))
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -805,7 +819,7 @@ class TestImage:
|
||||||
assert exif[296] == 2
|
assert exif[296] == 2
|
||||||
assert exif[11] == "gThumb 3.0.1"
|
assert exif[11] == "gThumb 3.0.1"
|
||||||
|
|
||||||
out = str(tmp_path / "temp.jpg")
|
out = tmp_path / "temp.jpg"
|
||||||
exif[258] = 8
|
exif[258] = 8
|
||||||
del exif[274]
|
del exif[274]
|
||||||
del exif[282]
|
del exif[282]
|
||||||
|
@ -827,7 +841,7 @@ class TestImage:
|
||||||
assert exif[274] == 1
|
assert exif[274] == 1
|
||||||
assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)"
|
assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)"
|
||||||
|
|
||||||
out = str(tmp_path / "temp.jpg")
|
out = tmp_path / "temp.jpg"
|
||||||
exif[258] = 8
|
exif[258] = 8
|
||||||
del exif[306]
|
del exif[306]
|
||||||
exif[274] = 455
|
exif[274] = 455
|
||||||
|
@ -846,7 +860,7 @@ class TestImage:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
assert exif == {}
|
assert exif == {}
|
||||||
|
|
||||||
out = str(tmp_path / "temp.webp")
|
out = tmp_path / "temp.webp"
|
||||||
exif[258] = 8
|
exif[258] = 8
|
||||||
exif[40963] = 455
|
exif[40963] = 455
|
||||||
exif[305] = "Pillow test"
|
exif[305] = "Pillow test"
|
||||||
|
@ -868,7 +882,7 @@ class TestImage:
|
||||||
exif = im.getexif()
|
exif = im.getexif()
|
||||||
assert exif == {274: 1}
|
assert exif == {274: 1}
|
||||||
|
|
||||||
out = str(tmp_path / "temp.png")
|
out = tmp_path / "temp.png"
|
||||||
exif[258] = 8
|
exif[258] = 8
|
||||||
del exif[274]
|
del exif[274]
|
||||||
exif[40963] = 455
|
exif[40963] = 455
|
||||||
|
|
|
@ -118,7 +118,7 @@ def test_trns_p(tmp_path: Path) -> None:
|
||||||
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_l = im.convert("L")
|
im_l = im.convert("L")
|
||||||
assert im_l.info["transparency"] == 0
|
assert im_l.info["transparency"] == 0
|
||||||
|
@ -154,7 +154,7 @@ def test_trns_l(tmp_path: Path) -> None:
|
||||||
im = hopper("L")
|
im = hopper("L")
|
||||||
im.info["transparency"] = 128
|
im.info["transparency"] = 128
|
||||||
|
|
||||||
f = str(tmp_path / "temp.png")
|
f = tmp_path / "temp.png"
|
||||||
|
|
||||||
im_la = im.convert("LA")
|
im_la = im.convert("LA")
|
||||||
assert "transparency" not in im_la.info
|
assert "transparency" not in im_la.info
|
||||||
|
@ -177,7 +177,7 @@ def test_trns_RGB(tmp_path: Path) -> None:
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
im.info["transparency"] = im.getpixel((0, 0))
|
im.info["transparency"] = im.getpixel((0, 0))
|
||||||
|
|
||||||
f = str(tmp_path / "temp.png")
|
f = tmp_path / "temp.png"
|
||||||
|
|
||||||
im_l = im.convert("L")
|
im_l = im.convert("L")
|
||||||
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone
|
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone
|
||||||
|
|
|
@ -462,7 +462,7 @@ class TestCoreResampleBox:
|
||||||
im.resize((32, 32), resample, (20, 20, 20, 100))
|
im.resize((32, 32), resample, (20, 20, 20, 100))
|
||||||
im.resize((32, 32), resample, (20, 20, 100, 20))
|
im.resize((32, 32), resample, (20, 20, 100, 20))
|
||||||
|
|
||||||
with pytest.raises(TypeError, match="must be sequence of length 4"):
|
with pytest.raises(TypeError, match="must be (sequence|tuple) of length 4"):
|
||||||
im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
|
im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="can't be negative"):
|
with pytest.raises(ValueError, match="can't be negative"):
|
||||||
|
|
|
@ -171,7 +171,7 @@ class TestImagingCoreResize:
|
||||||
# platforms. So if a future Pillow change requires that the test file
|
# platforms. So if a future Pillow change requires that the test file
|
||||||
# be updated, that is okay.
|
# be updated, that is okay.
|
||||||
im = hopper().resize((64, 64))
|
im = hopper().resize((64, 64))
|
||||||
temp_file = str(tmp_path / "temp.gif")
|
temp_file = tmp_path / "temp.gif"
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
||||||
with Image.open(temp_file) as reloaded:
|
with Image.open(temp_file) as reloaded:
|
||||||
|
|
|
@ -45,9 +45,9 @@ def test_split_merge(mode: str) -> None:
|
||||||
|
|
||||||
def test_split_open(tmp_path: Path) -> None:
|
def test_split_open(tmp_path: Path) -> None:
|
||||||
if features.check("zlib"):
|
if features.check("zlib"):
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = tmp_path / "temp.png"
|
||||||
else:
|
else:
|
||||||
test_file = str(tmp_path / "temp.pcx")
|
test_file = tmp_path / "temp.pcx"
|
||||||
|
|
||||||
def split_open(mode: str) -> int:
|
def split_open(mode: str) -> int:
|
||||||
hopper(mode).save(test_file)
|
hopper(mode).save(test_file)
|
||||||
|
|
|
@ -1704,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None:
|
||||||
BLACK,
|
BLACK,
|
||||||
)
|
)
|
||||||
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
|
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
|
||||||
assert_image_similar_tofile(img, expected, 1)
|
assert_image_equal_tofile(img, expected)
|
||||||
|
|
||||||
|
|
||||||
def test_polygon2() -> None:
|
def test_polygon2() -> None:
|
||||||
|
|
|
@ -131,6 +131,26 @@ class TestImageFile:
|
||||||
|
|
||||||
assert_image_equal(im1, im2)
|
assert_image_equal(im1, im2)
|
||||||
|
|
||||||
|
def test_tile_size(self) -> None:
|
||||||
|
with open("Tests/images/hopper.tif", "rb") as im_fp:
|
||||||
|
data = im_fp.read()
|
||||||
|
|
||||||
|
reads = []
|
||||||
|
|
||||||
|
class FP(BytesIO):
|
||||||
|
def read(self, size: int | None = None) -> bytes:
|
||||||
|
reads.append(size)
|
||||||
|
return super().read(size)
|
||||||
|
|
||||||
|
fp = FP(data)
|
||||||
|
with Image.open(fp) as im:
|
||||||
|
assert len(im.tile) == 7
|
||||||
|
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
# Despite multiple tiles, assert only one tile caused a read of maxblock size
|
||||||
|
assert reads.count(im.decodermaxblock) == 1
|
||||||
|
|
||||||
def test_raise_oserror(self) -> None:
|
def test_raise_oserror(self) -> None:
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -124,7 +124,7 @@ def test_render_equal(layout_engine: ImageFont.Layout) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None:
|
def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None:
|
||||||
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
|
tempfile = tmp_path / ("temp_" + chr(128) + ".ttf")
|
||||||
try:
|
try:
|
||||||
shutil.copy(FONT_PATH, tempfile)
|
shutil.copy(FONT_PATH, tempfile)
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
|
|
|
@ -7,7 +7,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont, _util, features
|
from PIL import Image, ImageDraw, ImageFont, _util, features
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile
|
from .helper import assert_image_equal_tofile, timeout_unless_slower_valgrind
|
||||||
|
|
||||||
fonts = [ImageFont.load_default_imagefont()]
|
fonts = [ImageFont.load_default_imagefont()]
|
||||||
if not features.check_module("freetype2"):
|
if not features.check_module("freetype2"):
|
||||||
|
@ -72,7 +72,7 @@ def test_decompression_bomb() -> None:
|
||||||
font.getmask("A" * 1_000_000)
|
font.getmask("A" * 1_000_000)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.timeout(4)
|
@timeout_unless_slower_valgrind(4)
|
||||||
def test_oom() -> None:
|
def test_oom() -> None:
|
||||||
glyph = struct.pack(
|
glyph = struct.pack(
|
||||||
">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767
|
">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user