mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-07-10 16:22:22 +03:00
Merge branch 'main' into progress
This commit is contained in:
commit
09e4df10af
|
@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
|
|||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
||||
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||
sway wl-clipboard libopenblas-dev
|
||||
sway wl-clipboard libopenblas-dev nasm
|
||||
fi
|
||||
|
||||
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-timeout
|
||||
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
|
||||
python3 -m pip install numpy
|
||||
|
@ -50,7 +53,7 @@ if [[ $(uname) != CYGWIN* ]]; then
|
|||
# Pyroma uses non-isolated build and fails with old setuptools
|
||||
if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
|
||||
# To match pyproject.toml
|
||||
python3 -m pip install "setuptools>=67.8"
|
||||
python3 -m pip install "setuptools>=77"
|
||||
fi
|
||||
|
||||
# webp
|
||||
|
@ -62,6 +65,9 @@ if [[ $(uname) != CYGWIN* ]]; then
|
|||
# raqm
|
||||
pushd depends && ./install_raqm.sh && popd
|
||||
|
||||
# libavif
|
||||
pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
else
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.23.1
|
||||
cibuildwheel==2.23.2
|
||||
|
|
|
@ -1,5 +1,26 @@
|
|||
# A clang-format style that approximates Python's PEP 7
|
||||
# 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
|
||||
AlwaysBreakAfterReturnType: All
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
|
@ -11,7 +32,6 @@ ColumnLimit: 88
|
|||
DerivePointerAlignment: false
|
||||
IndentGotoLabels: false
|
||||
IndentWidth: 4
|
||||
Language: Cpp
|
||||
PointerAlignment: Right
|
||||
ReflowComments: true
|
||||
SortIncludes: false
|
||||
|
|
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
|
||||
fi
|
||||
brew install \
|
||||
aom \
|
||||
dav1d \
|
||||
freetype \
|
||||
ghostscript \
|
||||
jpeg-turbo \
|
||||
|
@ -14,6 +16,8 @@ brew install \
|
|||
libtiff \
|
||||
little-cms2 \
|
||||
openjpeg \
|
||||
rav1e \
|
||||
svt-av1 \
|
||||
webp
|
||||
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 pyroma
|
||||
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
|
||||
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,
|
||||
debian-12-bookworm-x86,
|
||||
debian-12-bookworm-amd64,
|
||||
fedora-40-amd64,
|
||||
fedora-41-amd64,
|
||||
fedora-42-amd64,
|
||||
gentoo,
|
||||
ubuntu-22.04-jammy-amd64,
|
||||
ubuntu-24.04-noble-amd64,
|
||||
|
|
1
.github/workflows/test-mingw.yml
vendored
1
.github/workflows/test-mingw.yml
vendored
|
@ -60,6 +60,7 @@ jobs:
|
|||
mingw-w64-x86_64-gcc \
|
||||
mingw-w64-x86_64-ghostscript \
|
||||
mingw-w64-x86_64-lcms2 \
|
||||
mingw-w64-x86_64-libavif \
|
||||
mingw-w64-x86_64-libimagequant \
|
||||
mingw-w64-x86_64-libjpeg-turbo \
|
||||
mingw-w64-x86_64-libraqm \
|
||||
|
|
10
.github/workflows/test-windows.yml
vendored
10
.github/workflows/test-windows.yml
vendored
|
@ -42,7 +42,7 @@ jobs:
|
|||
# Test the oldest Python on 32-bit
|
||||
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
|
||||
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
|
||||
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
|
||||
|
||||
|
@ -88,6 +88,10 @@ jobs:
|
|||
run: |
|
||||
python3 -m pip install PyQt6
|
||||
|
||||
- name: Install PyArrow dependency
|
||||
run: |
|
||||
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
run: |
|
||||
|
@ -145,6 +149,10 @@ jobs:
|
|||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
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
|
||||
- name: Build dependencies / brotli
|
||||
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
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: Quansight-Labs/setup-python@v5
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
|
|
21
.github/workflows/wheels-dependencies.sh
vendored
21
.github/workflows/wheels-dependencies.sh
vendored
|
@ -25,7 +25,7 @@ else
|
|||
MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
|
||||
MB_ML_VER=${AUDITWHEEL_POLICY:9}
|
||||
fi
|
||||
PLAT=$CIBW_ARCHS
|
||||
PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
|
||||
|
||||
# Define custom utilities
|
||||
source wheels/multibuild/common_utils.sh
|
||||
|
@ -38,13 +38,14 @@ ARCHIVE_SDIR=pillow-depends-main
|
|||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.3
|
||||
HARFBUZZ_VERSION=10.4.0
|
||||
HARFBUZZ_VERSION=11.1.0
|
||||
LIBPNG_VERSION=1.6.47
|
||||
JPEGTURBO_VERSION=3.1.0
|
||||
OPENJPEG_VERSION=2.5.3
|
||||
XZ_VERSION=5.6.4
|
||||
XZ_VERSION=5.8.1
|
||||
TIFF_VERSION=4.7.0
|
||||
LCMS2_VERSION=2.17
|
||||
ZLIB_VERSION=1.3.1
|
||||
ZLIB_NG_VERSION=2.2.4
|
||||
LIBWEBP_VERSION=1.5.0
|
||||
BZIP2_VERSION=1.0.8
|
||||
|
@ -64,11 +65,7 @@ function build_pkg_config {
|
|||
|
||||
function build_zlib_ng {
|
||||
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
|
||||
(cd zlib-ng-$ZLIB_NG_VERSION \
|
||||
&& ./configure --prefix=$BUILD_PREFIX --zlib-compat \
|
||||
&& make -j4 \
|
||||
&& make install)
|
||||
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat
|
||||
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
# 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)
|
||||
(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 \
|
||||
&& meson install)
|
||||
touch harfbuzz-stamp
|
||||
|
@ -106,7 +103,11 @@ function build {
|
|||
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||
yum remove -y zlib-devel
|
||||
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
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
|
|
9
.github/workflows/wheels.yml
vendored
9
.github/workflows/wheels.yml
vendored
|
@ -121,14 +121,17 @@ jobs:
|
|||
windows:
|
||||
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
|
||||
name: Windows ${{ matrix.cibw_arch }}
|
||||
runs-on: windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- cibw_arch: x86
|
||||
os: windows-latest
|
||||
- cibw_arch: AMD64
|
||||
os: windows-latest
|
||||
- cibw_arch: ARM64
|
||||
os: windows-11-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
@ -157,7 +160,7 @@ jobs:
|
|||
# Install extra test 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
|
||||
|
||||
- name: Build wheels
|
||||
|
@ -240,7 +243,7 @@ jobs:
|
|||
path: dist
|
||||
merge-multiple: true
|
||||
- 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:
|
||||
artifacts_path: dist
|
||||
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.9
|
||||
rev: v0.11.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
@ -24,7 +24,7 @@ repos:
|
|||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v19.1.7
|
||||
rev: v20.1.0
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types: [c]
|
||||
|
@ -44,20 +44,21 @@ repos:
|
|||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^Tests/images/
|
||||
- id: trailing-whitespace
|
||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.31.2
|
||||
rev: 0.32.1
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.4.1
|
||||
rev: v1.5.2
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
|
@ -72,7 +73,7 @@ repos:
|
|||
- id: pyproject-fmt
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.23
|
||||
rev: v0.24.1
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
additional_dependencies: [trove-classifiers>=2024.10.12]
|
||||
|
|
5
Makefile
5
Makefile
|
@ -23,6 +23,10 @@ doc html:
|
|||
htmlview:
|
||||
$(MAKE) -C docs htmlview
|
||||
|
||||
.PHONY: htmllive
|
||||
htmllive:
|
||||
$(MAKE) -C docs htmllive
|
||||
|
||||
.PHONY: doccheck
|
||||
doccheck:
|
||||
$(MAKE) doc
|
||||
|
@ -43,6 +47,7 @@ help:
|
|||
@echo " docserve run an HTTP server on the docs directory"
|
||||
@echo " html make HTML docs"
|
||||
@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-coverage make and install with C coverage"
|
||||
@echo " lint run the lint checks"
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from PIL import features
|
||||
|
||||
from .helper import is_pypy
|
||||
|
||||
|
||||
def test_wheel_modules() -> None:
|
||||
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
|
||||
|
@ -40,5 +43,7 @@ def test_wheel_features() -> None:
|
|||
|
||||
if sys.platform == "win32":
|
||||
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
|
||||
|
|
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 |
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)
|
|
@ -13,6 +13,7 @@ from PIL import Image, ImageSequence, PngImagePlugin
|
|||
# (referenced from https://wiki.mozilla.org/APNG_Specification)
|
||||
def test_apng_basic() -> None:
|
||||
with Image.open("Tests/images/apng/single_frame.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
assert im.n_frames == 1
|
||||
assert im.get_format_mimetype() == "image/apng"
|
||||
|
@ -21,6 +22,7 @@ def test_apng_basic() -> None:
|
|||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/single_frame_default.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.is_animated
|
||||
assert im.n_frames == 2
|
||||
assert im.get_format_mimetype() == "image/apng"
|
||||
|
@ -53,6 +55,7 @@ def test_apng_basic() -> None:
|
|||
)
|
||||
def test_apng_fdat(filename: str) -> None:
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
@ -60,31 +63,37 @@ def test_apng_fdat(filename: str) -> None:
|
|||
|
||||
def test_apng_dispose() -> None:
|
||||
with Image.open("Tests/images/apng/dispose_op_none.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
@ -92,21 +101,25 @@ def test_apng_dispose() -> None:
|
|||
|
||||
def test_apng_dispose_region() -> None:
|
||||
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
||||
with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
@ -133,6 +146,7 @@ def test_apng_dispose_op_previous_frame() -> None:
|
|||
# ],
|
||||
# )
|
||||
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
|
||||
|
||||
|
@ -146,26 +160,31 @@ def test_apng_dispose_op_background_p_mode() -> None:
|
|||
|
||||
def test_apng_blend() -> None:
|
||||
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 97)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
@ -179,6 +198,7 @@ def test_apng_blend_transparency() -> None:
|
|||
|
||||
def test_apng_chunk_order() -> None:
|
||||
with Image.open("Tests/images/apng/fctl_actl.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
@ -234,24 +254,28 @@ def test_apng_num_plays() -> None:
|
|||
|
||||
def test_apng_mode() -> None:
|
||||
with Image.open("Tests/images/apng/mode_16bit.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "RGBA"
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 128, 191)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 128, 191)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_grayscale.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "L"
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == 128
|
||||
assert im.getpixel((64, 32)) == 255
|
||||
|
||||
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "LA"
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (128, 191)
|
||||
assert im.getpixel((64, 32)) == (128, 191)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_palette.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGB")
|
||||
|
@ -259,6 +283,7 @@ def test_apng_mode() -> None:
|
|||
assert im.getpixel((64, 32)) == (0, 255, 0)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGBA")
|
||||
|
@ -266,6 +291,7 @@ def test_apng_mode() -> None:
|
|||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGBA")
|
||||
|
@ -275,25 +301,31 @@ def test_apng_mode() -> None:
|
|||
|
||||
def test_apng_chunk_errors() -> None:
|
||||
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
|
||||
im.load()
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open("Tests/images/apng/chunk_no_fctl.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
with Image.open("Tests/images/apng/chunk_no_fdat.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
|
@ -301,26 +333,31 @@ def test_apng_chunk_errors() -> None:
|
|||
def test_apng_syntax_errors() -> None:
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
im.load()
|
||||
|
||||
# we can handle this case gracefully
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
with pytest.raises(OSError):
|
||||
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.load()
|
||||
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
im.load()
|
||||
|
||||
|
@ -340,6 +377,7 @@ def test_apng_syntax_errors() -> None:
|
|||
def test_apng_sequence_errors(test_file: str) -> None:
|
||||
with pytest.raises(SyntaxError):
|
||||
with Image.open(f"Tests/images/apng/{test_file}") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
|
||||
|
@ -350,6 +388,7 @@ def test_apng_save(tmp_path: Path) -> None:
|
|||
im.save(test_file, save_all=True)
|
||||
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.load()
|
||||
assert not im.is_animated
|
||||
assert im.n_frames == 1
|
||||
|
@ -365,6 +404,7 @@ def test_apng_save(tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.load()
|
||||
assert im.is_animated
|
||||
assert im.n_frames == 2
|
||||
|
@ -404,6 +444,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
|
|||
append_images=frames,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
|
||||
|
@ -446,6 +487,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
|||
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert "duration" not in im.info
|
||||
|
||||
|
@ -457,6 +499,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
|||
duration=[500, 100, 150],
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.n_frames == 2
|
||||
assert im.info["duration"] == 600
|
||||
|
||||
|
@ -467,6 +510,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
|||
frame.info["duration"] = 300
|
||||
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.n_frames == 2
|
||||
assert im.info["duration"] == 600
|
||||
|
||||
|
|
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 <= 3
|
||||
|
||||
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)
|
|
@ -15,25 +15,19 @@ from .helper import (
|
|||
)
|
||||
|
||||
|
||||
def test_sanity(tmp_path: Path) -> None:
|
||||
def roundtrip(im: Image.Image) -> None:
|
||||
outfile = tmp_path / "temp.bmp"
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
|
||||
def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||
outfile = tmp_path / "temp.bmp"
|
||||
|
||||
im.save(outfile, "BMP")
|
||||
im = hopper(mode)
|
||||
im.save(outfile, "BMP")
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
reloaded.load()
|
||||
assert im.mode == reloaded.mode
|
||||
assert im.size == reloaded.size
|
||||
assert reloaded.format == "BMP"
|
||||
assert reloaded.get_format_mimetype() == "image/bmp"
|
||||
|
||||
roundtrip(hopper())
|
||||
|
||||
roundtrip(hopper("1"))
|
||||
roundtrip(hopper("L"))
|
||||
roundtrip(hopper("P"))
|
||||
roundtrip(hopper("RGB"))
|
||||
with Image.open(outfile) as reloaded:
|
||||
reloaded.load()
|
||||
assert im.mode == reloaded.mode
|
||||
assert im.size == reloaded.size
|
||||
assert reloaded.format == "BMP"
|
||||
assert reloaded.get_format_mimetype() == "image/bmp"
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
|
@ -196,9 +190,9 @@ def test_rle8() -> None:
|
|||
# Signal end of bitmap before the image is finished
|
||||
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
|
||||
data = fp.read(1063) + b"\x01"
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_rle4() -> None:
|
||||
|
@ -220,9 +214,9 @@ def test_rle4() -> None:
|
|||
def test_rle8_eof(file_name: str, length: int) -> None:
|
||||
with open(file_name, "rb") as fp:
|
||||
data = fp.read(length)
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_offset() -> None:
|
||||
|
@ -230,3 +224,13 @@ def test_offset() -> None:
|
|||
# to exclude the palette size from the pixel data offset
|
||||
with Image.open("Tests/images/pal8_offset.bmp") as im:
|
||||
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"
|
||||
|
|
|
@ -69,12 +69,14 @@ def test_tell() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, DcxImagePlugin.DcxImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, DcxImagePlugin.DcxImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
|
|
@ -86,6 +86,8 @@ simple_eps_file_with_long_binary_data = (
|
|||
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
|
||||
expected_size = tuple(s * scale for s in size)
|
||||
with Image.open(filename) as image:
|
||||
assert isinstance(image, EpsImagePlugin.EpsImageFile)
|
||||
|
||||
image.load(scale=scale)
|
||||
assert image.mode == "RGB"
|
||||
assert image.size == expected_size
|
||||
|
@ -227,6 +229,8 @@ def test_showpage() -> None:
|
|||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_transparency() -> None:
|
||||
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
|
||||
assert isinstance(plot_image, EpsImagePlugin.EpsImageFile)
|
||||
|
||||
plot_image.load(transparency=True)
|
||||
assert plot_image.mode == "RGBA"
|
||||
|
||||
|
@ -308,6 +312,7 @@ def test_render_scale2() -> None:
|
|||
|
||||
# Zero bounding box
|
||||
with Image.open(FILE1) as image1_scale2:
|
||||
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
|
||||
image1_scale2.load(scale=2)
|
||||
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
|
||||
image1_scale2_compare = image1_scale2_compare.convert("RGB")
|
||||
|
@ -316,6 +321,7 @@ def test_render_scale2() -> None:
|
|||
|
||||
# Non-zero bounding box
|
||||
with Image.open(FILE2) as image2_scale2:
|
||||
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
|
||||
image2_scale2.load(scale=2)
|
||||
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
|
||||
image2_scale2_compare = image2_scale2_compare.convert("RGB")
|
||||
|
|
|
@ -22,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
|
|||
|
||||
def test_sanity() -> None:
|
||||
with Image.open(static_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
|
||||
im.load()
|
||||
assert im.mode == "P"
|
||||
assert im.size == (128, 128)
|
||||
|
@ -29,6 +31,8 @@ def test_sanity() -> None:
|
|||
assert not im.is_animated
|
||||
|
||||
with Image.open(animated_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
|
||||
assert im.mode == "P"
|
||||
assert im.size == (320, 200)
|
||||
assert im.format == "FLI"
|
||||
|
@ -112,16 +116,19 @@ def test_palette_chunk_second() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(static_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open(animated_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
assert im.n_frames == 384
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(animated_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
@ -166,6 +173,7 @@ def test_seek_tell() -> None:
|
|||
|
||||
def test_seek() -> None:
|
||||
with Image.open(animated_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
im.seek(50)
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/a_fli.png")
|
||||
|
|
|
@ -22,10 +22,11 @@ def test_sanity() -> None:
|
|||
|
||||
def test_close() -> None:
|
||||
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
||||
pass
|
||||
assert isinstance(im, FpxImagePlugin.FpxImageFile)
|
||||
assert im.ole.fp.closed
|
||||
|
||||
im = Image.open("Tests/images/input_bw_one_band.fpx")
|
||||
assert isinstance(im, FpxImagePlugin.FpxImageFile)
|
||||
im.close()
|
||||
assert im.ole.fp.closed
|
||||
|
||||
|
|
|
@ -450,6 +450,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
|
|||
|
||||
def test_seek() -> None:
|
||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
frame_count = 0
|
||||
try:
|
||||
while True:
|
||||
|
@ -494,10 +495,12 @@ def test_seek_rewind() -> None:
|
|||
def test_n_frames(path: str, n_frames: int) -> None:
|
||||
# Test is_animated before n_frames
|
||||
with Image.open(path) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
# Test is_animated after n_frames
|
||||
with Image.open(path) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
|
@ -507,6 +510,7 @@ def test_no_change() -> None:
|
|||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
im.seek(1)
|
||||
expected = im.copy()
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.n_frames == 5
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
|
@ -514,17 +518,20 @@ def test_no_change() -> None:
|
|||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
im.seek(3)
|
||||
expected = im.copy()
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.is_animated
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
|
||||
expected = Image.new("P", (1, 1))
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert not im.is_animated
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(TEST_GIF) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
@ -543,6 +550,7 @@ def test_first_frame_transparency() -> None:
|
|||
|
||||
def test_dispose_none() -> None:
|
||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
try:
|
||||
while True:
|
||||
img.seek(img.tell() + 1)
|
||||
|
@ -566,6 +574,7 @@ def test_dispose_none_load_end() -> None:
|
|||
|
||||
def test_dispose_background() -> None:
|
||||
with Image.open("Tests/images/dispose_bgnd.gif") as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
try:
|
||||
while True:
|
||||
img.seek(img.tell() + 1)
|
||||
|
@ -619,6 +628,7 @@ def test_transparent_dispose(
|
|||
|
||||
def test_dispose_previous() -> None:
|
||||
with Image.open("Tests/images/dispose_prev.gif") as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
try:
|
||||
while True:
|
||||
img.seek(img.tell() + 1)
|
||||
|
@ -656,6 +666,7 @@ def test_save_dispose(tmp_path: Path) -> None:
|
|||
for method in range(4):
|
||||
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
|
||||
with Image.open(out) as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
for _ in range(2):
|
||||
img.seek(img.tell() + 1)
|
||||
assert img.disposal_method == method
|
||||
|
@ -669,6 +680,7 @@ def test_save_dispose(tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
with Image.open(out) as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
for i in range(2):
|
||||
img.seek(img.tell() + 1)
|
||||
assert img.disposal_method == i + 1
|
||||
|
@ -791,6 +803,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
|
|||
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
|
||||
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.n_frames == 3
|
||||
|
||||
|
||||
|
@ -972,6 +985,8 @@ def test_identical_frames(tmp_path: Path) -> None:
|
|||
out, save_all=True, append_images=im_list[1:], duration=duration_list
|
||||
)
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
|
||||
# Assert that the first three frames were combined
|
||||
assert reread.n_frames == 2
|
||||
|
||||
|
@ -1001,6 +1016,8 @@ def test_identical_frames_to_single_frame(
|
|||
|
||||
im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
|
||||
# Assert that all frames were combined
|
||||
assert reread.n_frames == 1
|
||||
|
||||
|
@ -1187,6 +1204,14 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
im.copy().save(out, save_all=True, append_images=ims)
|
||||
|
||||
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
|
||||
|
||||
# Tests appending using a generator
|
||||
|
@ -1196,6 +1221,7 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
im.save(out, save_all=True, append_images=im_generator(ims))
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 3
|
||||
|
||||
# Tests appending single and multiple frame images
|
||||
|
@ -1204,6 +1230,7 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
im.save(out, save_all=True, append_images=[im2])
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 10
|
||||
|
||||
|
||||
|
@ -1304,6 +1331,7 @@ def test_bbox(tmp_path: Path) -> None:
|
|||
im.save(out, save_all=True, append_images=ims)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 2
|
||||
|
||||
|
||||
|
@ -1316,6 +1344,7 @@ def test_bbox_alpha(tmp_path: Path) -> None:
|
|||
im.save(out, save_all=True, append_images=[im2])
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 2
|
||||
|
||||
|
||||
|
@ -1467,6 +1496,7 @@ def test_extents(
|
|||
) -> None:
|
||||
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
|
||||
with Image.open("Tests/images/" + test_file) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.size == (100, 100)
|
||||
|
||||
# Check that n_frames does not change the size
|
||||
|
@ -1514,4 +1544,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
|
|||
im1.save(out, save_all=True, append_images=[im2], **params)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, GifImagePlugin.GifImageFile)
|
||||
assert reloaded.n_frames == 2
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL.GimpPaletteFile import GimpPaletteFile
|
||||
|
@ -14,17 +16,20 @@ def test_sanity() -> None:
|
|||
GimpPaletteFile(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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
|
||||
with open("Tests/images/" + filename, "rb") as fp:
|
||||
palette_file = GimpPaletteFile(fp)
|
||||
|
||||
# Act
|
||||
|
@ -32,4 +37,36 @@ def test_get_palette() -> None:
|
|||
|
||||
# Assert
|
||||
assert mode == "RGB"
|
||||
assert len(palette) / 3 == 8
|
||||
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_save() -> None:
|
|||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
dummy_fp = BytesIO()
|
||||
dummy_filename = "dummy.filename"
|
||||
dummy_filename = "dummy.h5"
|
||||
|
||||
# Act / Assert: stub cannot save without an implemented handler
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -69,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None:
|
|||
assert_image_similar_tofile(im, temp_file, 1)
|
||||
|
||||
with Image.open(temp_file) as reread:
|
||||
assert isinstance(reread, IcnsImagePlugin.IcnsImageFile)
|
||||
reread.size = (16, 16)
|
||||
reread.load(2)
|
||||
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
|
||||
# dimensions are as expected
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, IcnsImagePlugin.IcnsImageFile)
|
||||
for w, h, r in im.info["sizes"]:
|
||||
wr = w * r
|
||||
hr = h * r
|
||||
|
@ -118,6 +120,7 @@ def test_older_icon() -> None:
|
|||
wr = w * r
|
||||
hr = h * r
|
||||
with Image.open("Tests/images/pillow2.icns") as im2:
|
||||
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
|
||||
im2.size = (w, h)
|
||||
im2.load(r)
|
||||
assert im2.mode == "RGBA"
|
||||
|
@ -135,6 +138,7 @@ def test_jp2_icon() -> None:
|
|||
wr = w * r
|
||||
hr = h * r
|
||||
with Image.open("Tests/images/pillow3.icns") as im2:
|
||||
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
|
||||
im2.size = (w, h)
|
||||
im2.load(r)
|
||||
assert im2.mode == "RGBA"
|
||||
|
|
|
@ -77,6 +77,7 @@ def test_save_to_bytes() -> None:
|
|||
# The other one
|
||||
output.seek(0)
|
||||
with Image.open(output) as reloaded:
|
||||
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
|
||||
reloaded.size = (32, 32)
|
||||
|
||||
assert im.mode == reloaded.mode
|
||||
|
@ -94,6 +95,7 @@ def test_getpixel(tmp_path: Path) -> None:
|
|||
im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
|
||||
|
||||
with Image.open(temp_file) as reloaded:
|
||||
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
|
||||
reloaded.load()
|
||||
reloaded.size = (32, 32)
|
||||
|
||||
|
@ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
|
|||
# The other one
|
||||
output.seek(0)
|
||||
with Image.open(output) as reloaded:
|
||||
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
|
||||
reloaded.size = (32, 32)
|
||||
|
||||
assert "RGBA" == reloaded.mode
|
||||
|
@ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
|
|||
|
||||
def test_incorrect_size() -> None:
|
||||
with Image.open(TEST_ICO_FILE) as im:
|
||||
assert isinstance(im, IcoImagePlugin.IcoImageFile)
|
||||
with pytest.raises(ValueError):
|
||||
im.size = (1, 1)
|
||||
|
||||
|
@ -219,6 +223,7 @@ def test_save_append_images(tmp_path: Path) -> None:
|
|||
im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im])
|
||||
|
||||
with Image.open(outfile) as reread:
|
||||
assert isinstance(reread, IcoImagePlugin.IcoImageFile)
|
||||
assert_image_equal(reread, hopper("RGBA"))
|
||||
|
||||
reread.size = (32, 32)
|
||||
|
|
|
@ -68,12 +68,14 @@ def test_tell() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(TEST_IM) as im:
|
||||
assert isinstance(im, ImImagePlugin.ImImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(TEST_IM) as im:
|
||||
assert isinstance(im, ImImagePlugin.ImImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
|
|
@ -91,6 +91,7 @@ class TestFileJpeg:
|
|||
def test_app(self) -> None:
|
||||
# Test APP/COM reader (@PIL135)
|
||||
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[1] == (
|
||||
"COM",
|
||||
|
@ -316,6 +317,8 @@ class TestFileJpeg:
|
|||
|
||||
def test_exif_typeerror(self) -> None:
|
||||
with Image.open("Tests/images/exif_typeerror.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
|
||||
# Should not raise a TypeError
|
||||
im._getexif()
|
||||
|
||||
|
@ -500,6 +503,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_mp(self) -> None:
|
||||
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
assert im._getmp() is None
|
||||
|
||||
def test_quality_keep(self, tmp_path: Path) -> None:
|
||||
|
@ -558,12 +562,14 @@ class TestFileJpeg:
|
|||
with Image.open(test_file) as im:
|
||||
im.save(b, "JPEG", qtables=[[n] * 64] * n)
|
||||
with Image.open(b) as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
assert len(im.quantization) == n
|
||||
reloaded = self.roundtrip(im, qtables="keep")
|
||||
assert im.quantization == reloaded.quantization
|
||||
assert max(reloaded.quantization[0]) <= 255
|
||||
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
qtables = im.quantization
|
||||
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
|
||||
assert im.quantization == reloaded.quantization
|
||||
|
@ -663,6 +669,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_load_16bit_qtables(self) -> None:
|
||||
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[0]) == 64
|
||||
assert max(im.quantization[0]) > 255
|
||||
|
@ -705,6 +712,7 @@ class TestFileJpeg:
|
|||
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
|
||||
def test_load_djpeg(self) -> None:
|
||||
with Image.open(TEST_FILE) as img:
|
||||
assert isinstance(img, JpegImagePlugin.JpegImageFile)
|
||||
img.load_djpeg()
|
||||
assert_image_similar_tofile(img, TEST_FILE, 5)
|
||||
|
||||
|
@ -909,6 +917,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_photoshop_malformed_and_multiple(self) -> None:
|
||||
with Image.open("Tests/images/app13-multiple.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
assert "photoshop" in im.info
|
||||
assert 24 == len(im.info["photoshop"])
|
||||
apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"]
|
||||
|
@ -1084,6 +1093,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_deprecation(self) -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert im.huffman_ac == {}
|
||||
with pytest.warns(DeprecationWarning):
|
||||
|
|
|
@ -228,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None:
|
|||
out.seek(0)
|
||||
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
|
||||
im.layers = 1
|
||||
im.load()
|
||||
assert_image_similar(im, card, 13)
|
||||
|
||||
out.seek(0)
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
|
||||
im.layers = 3
|
||||
im.load()
|
||||
assert_image_similar(im, card, 0.4)
|
||||
|
@ -455,8 +457,8 @@ def test_comment() -> None:
|
|||
# Test an image that is truncated partway through a codestream
|
||||
with open("Tests/images/comment.jp2", "rb") as fp:
|
||||
b = BytesIO(fp.read(130))
|
||||
with Image.open(b) as im:
|
||||
pass
|
||||
with Image.open(b) as im:
|
||||
pass
|
||||
|
||||
|
||||
def test_save_comment(card: ImageFile.ImageFile) -> None:
|
||||
|
|
|
@ -36,6 +36,7 @@ class LibTiffTestCase:
|
|||
im.load()
|
||||
im.getdata()
|
||||
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im._compression == "group4"
|
||||
|
||||
# can we write it back out, in a different form.
|
||||
|
@ -80,7 +81,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
s = io.BytesIO()
|
||||
with open(test_file, "rb") as f:
|
||||
s.write(f.read())
|
||||
s.seek(0)
|
||||
s.seek(0)
|
||||
with Image.open(s) as im:
|
||||
assert im.size == (500, 500)
|
||||
self._assert_noerr(tmp_path, im)
|
||||
|
@ -153,6 +154,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
"""Test metadata writing through libtiff"""
|
||||
f = tmp_path / "temp.tiff"
|
||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||
img.save(f, tiffinfo=img.tag)
|
||||
|
||||
if legacy_api:
|
||||
|
@ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
]
|
||||
|
||||
with Image.open(f) as loaded:
|
||||
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||
if legacy_api:
|
||||
reloaded = loaded.tag.named()
|
||||
else:
|
||||
|
@ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Exclude ones that have special meaning
|
||||
# that we're already testing them
|
||||
with Image.open("Tests/images/hopper_g4.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
for tag in im.tag_v2:
|
||||
try:
|
||||
del core_items[tag]
|
||||
|
@ -317,6 +321,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.save(out, tiffinfo=tiffinfo)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
for tag, value in tiffinfo.items():
|
||||
reloaded_value = reloaded.tag_v2[tag]
|
||||
if (
|
||||
|
@ -349,12 +354,14 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
def test_osubfiletype(self, tmp_path: Path) -> None:
|
||||
outfile = tmp_path / "temp.tif"
|
||||
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.tag_v2[OSUBFILETYPE] = 1
|
||||
im.save(outfile)
|
||||
|
||||
def test_subifd(self, tmp_path: Path) -> None:
|
||||
outfile = tmp_path / "temp.tif"
|
||||
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.tag_v2[SUBIFD] = 10000
|
||||
|
||||
# Should not segfault
|
||||
|
@ -369,6 +376,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
if 700 in reloaded.tag_v2:
|
||||
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
||||
|
||||
|
@ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
"""Tests String data in info directory"""
|
||||
test_file = "Tests/images/hopper_g4_500.tif"
|
||||
with Image.open(test_file) as orig:
|
||||
assert isinstance(orig, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
out = tmp_path / "temp.tif"
|
||||
|
||||
orig.tag[269] = "temp.tif"
|
||||
orig.save(out)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||
assert "temp.tif" == reread.tag_v2[269]
|
||||
assert "temp.tif" == reread.tag[269][0]
|
||||
|
||||
|
@ -541,6 +552,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
with Image.open(out) as reloaded:
|
||||
# colormap/palette tag
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert len(reloaded.tag_v2[320]) == 768
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
|
||||
|
@ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
|
||||
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.seek(0)
|
||||
assert im.size == (10, 10)
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
|
||||
|
@ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# issue #862
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
frames = im.n_frames
|
||||
assert frames == 3
|
||||
for _ in range(frames):
|
||||
|
@ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert not im.tag.next
|
||||
im.load()
|
||||
assert not im.tag.next
|
||||
|
@ -690,21 +705,25 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.save(outfile, compression="jpeg")
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[530] == (1, 1)
|
||||
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
||||
|
||||
def test_exif_ifd(self) -> None:
|
||||
out = io.BytesIO()
|
||||
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2[34665] == 125456
|
||||
im.save(out, "TIFF")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert 34665 not in reloaded.tag_v2
|
||||
|
||||
im.save(out, "TIFF", tiffinfo={34665: 125456})
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
if Image.core.libtiff_support_custom_tags:
|
||||
assert reloaded.tag_v2[34665] == 125456
|
||||
|
||||
|
@ -786,6 +805,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
def test_multipage_compression(self) -> None:
|
||||
with Image.open("Tests/images/compression.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.seek(0)
|
||||
assert im._compression == "tiff_ccitt"
|
||||
assert im.size == (10, 10)
|
||||
|
@ -1026,6 +1046,17 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open("Tests/images/old-style-jpeg-compression.tif") as im:
|
||||
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:
|
||||
with Image.open(
|
||||
"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:
|
||||
for i in range(2, 9):
|
||||
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert 274 in im.tag_v2
|
||||
|
||||
im.load()
|
||||
|
|
|
@ -32,7 +32,7 @@ class TestFileLibTiffSmall(LibTiffTestCase):
|
|||
s = BytesIO()
|
||||
with open(test_file, "rb") as f:
|
||||
s.write(f.read())
|
||||
s.seek(0)
|
||||
s.seek(0)
|
||||
with Image.open(s) as im:
|
||||
assert im.size == (128, 128)
|
||||
self._assert_noerr(tmp_path, im)
|
||||
|
|
|
@ -30,11 +30,13 @@ def test_sanity() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||
assert im.n_frames == 1
|
||||
|
||||
|
||||
def test_is_animated() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||
assert not im.is_animated
|
||||
|
||||
|
||||
|
@ -55,10 +57,11 @@ def test_seek() -> None:
|
|||
|
||||
def test_close() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
pass
|
||||
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||
assert im.ole.fp.closed
|
||||
|
||||
im = Image.open(TEST_FILE)
|
||||
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||
im.close()
|
||||
assert im.ole.fp.closed
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from typing import Any
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFile, MpoImagePlugin
|
||||
from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -80,6 +80,7 @@ def test_context_manager() -> None:
|
|||
def test_app(test_file: str) -> None:
|
||||
# Test APP/COM reader (@PIL135)
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
assert im.applist[0][0] == "APP1"
|
||||
assert im.applist[1][0] == "APP2"
|
||||
assert im.applist[1][1].startswith(
|
||||
|
@ -220,12 +221,14 @@ def test_seek(test_file: str) -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open("Tests/images/sugarshack.mpo") as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
assert im.n_frames == 2
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open("Tests/images/sugarshack.mpo") as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
@ -239,6 +242,8 @@ def test_eoferror() -> None:
|
|||
|
||||
def test_adopt_jpeg() -> None:
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
MpoImagePlugin.MpoImageFile.adopt(im)
|
||||
|
||||
|
|
|
@ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None:
|
|||
|
||||
im.save(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)
|
||||
|
||||
|
||||
|
@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None:
|
|||
roundtrip(tmp_path, mode)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Palm P image is wrong")
|
||||
def test_p_mode(tmp_path: Path) -> None:
|
||||
# Arrange
|
||||
mode = "P"
|
||||
|
|
|
@ -576,6 +576,7 @@ class TestFilePng:
|
|||
|
||||
def test_read_private_chunks(self) -> None:
|
||||
with Image.open("Tests/images/exif.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.private_chunks == [(b"orNT", b"\x01")]
|
||||
|
||||
def test_roundtrip_private_chunk(self) -> None:
|
||||
|
@ -598,6 +599,7 @@ class TestFilePng:
|
|||
|
||||
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with Image.open("Tests/images/hopper.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert "comment" in im.text
|
||||
for k, v in {
|
||||
"date:create": "2014-09-04T09:37:08+03:00",
|
||||
|
@ -607,15 +609,19 @@ class TestFilePng:
|
|||
|
||||
# Raises a SyntaxError in load_end
|
||||
with Image.open("Tests/images/broken_data_stream.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
with pytest.raises(OSError):
|
||||
assert isinstance(im.text, dict)
|
||||
|
||||
# Raises an EOFError in load_end
|
||||
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"}
|
||||
|
||||
# Raises a UnicodeDecodeError in load_end
|
||||
with Image.open("Tests/images/truncated_image.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
|
||||
# The file is truncated
|
||||
with pytest.raises(OSError):
|
||||
im.text
|
||||
|
@ -726,6 +732,7 @@ class TestFilePng:
|
|||
im.save(test_file)
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
|
||||
assert reloaded._getexif() is None
|
||||
|
||||
# Test passing in exif
|
||||
|
|
|
@ -59,17 +59,21 @@ def test_invalid_file() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open("Tests/images/hopper_merged.psd") as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
|
||||
with Image.open(path) as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
assert im.n_frames == 2
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
|
||||
# PSD seek index starts at 1 rather than 0
|
||||
n_frames = im.n_frames + 1
|
||||
|
||||
|
@ -119,11 +123,13 @@ def test_rgba() -> None:
|
|||
|
||||
def test_negative_top_left_layer() -> None:
|
||||
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)
|
||||
|
||||
|
||||
def test_layer_skip() -> None:
|
||||
with Image.open("Tests/images/five_channels.psd") as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
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:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.layers
|
||||
|
|
|
@ -71,24 +71,26 @@ def test_invalid_file() -> None:
|
|||
SgiImagePlugin.SgiImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_write(tmp_path: Path) -> None:
|
||||
def roundtrip(img: Image.Image) -> None:
|
||||
out = tmp_path / "temp.sgi"
|
||||
img.save(out, format="sgi")
|
||||
def roundtrip(img: Image.Image, tmp_path: Path) -> None:
|
||||
out = tmp_path / "temp.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)
|
||||
|
||||
out = tmp_path / "fp.sgi"
|
||||
with open(out, "wb") as fp:
|
||||
img.save(fp)
|
||||
assert_image_equal_tofile(img, out)
|
||||
assert not fp.closed
|
||||
|
||||
assert not fp.closed
|
||||
|
||||
for mode in ("L", "RGB", "RGBA"):
|
||||
roundtrip(hopper(mode))
|
||||
@pytest.mark.parametrize("mode", ("L", "RGB", "RGBA"))
|
||||
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:
|
||||
|
|
|
@ -96,6 +96,7 @@ def test_tell() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, SpiderImagePlugin.SpiderImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from glob import glob
|
||||
from itertools import product
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
@ -15,14 +13,27 @@ _TGA_DIR = os.path.join("Tests", "images", "tga")
|
|||
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
|
||||
|
||||
|
||||
_MODES = ("L", "LA", "P", "RGB", "RGBA")
|
||||
_ORIGINS = ("tl", "bl")
|
||||
|
||||
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", _MODES)
|
||||
def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"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:
|
||||
out = tmp_path / "temp.tga"
|
||||
|
||||
|
@ -36,33 +47,26 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
|
|||
|
||||
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:
|
||||
with Image.open(png_path) as reference_im:
|
||||
assert reference_im.mode == mode
|
||||
path_no_ext = os.path.splitext(png_path)[0]
|
||||
tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw")
|
||||
|
||||
path_no_ext = os.path.splitext(png_path)[0]
|
||||
for origin, rle in product(_ORIGINS, (True, False)):
|
||||
tga_path = "{}_{}_{}.tga".format(
|
||||
path_no_ext, origin, "rle" if rle else "raw"
|
||||
)
|
||||
with Image.open(tga_path) as original_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()
|
||||
|
||||
with Image.open(tga_path) as original_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)
|
||||
|
||||
assert_image_equal(original_im, reference_im)
|
||||
|
||||
roundtrip(original_im)
|
||||
roundtrip(original_im)
|
||||
|
||||
|
||||
def test_palette_depth_8() -> None:
|
||||
|
|
|
@ -9,7 +9,13 @@ from types import ModuleType
|
|||
|
||||
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 .helper import (
|
||||
|
@ -113,6 +119,7 @@ class TestFileTiff:
|
|||
|
||||
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
||||
outfile = tmp_path / "temp.tif"
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||
|
||||
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
||||
|
@ -121,11 +128,13 @@ class TestFileTiff:
|
|||
im.save(outfile, big_tiff=True)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2._bigtiff is True
|
||||
|
||||
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2._bigtiff is True
|
||||
|
||||
def test_seek_too_large(self) -> None:
|
||||
|
@ -140,6 +149,8 @@ class TestFileTiff:
|
|||
def test_xyres_tiff(self) -> None:
|
||||
filename = "Tests/images/pil168.tif"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# legacy api
|
||||
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
|
||||
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
|
||||
|
@ -153,6 +164,8 @@ class TestFileTiff:
|
|||
def test_xyres_fallback_tiff(self) -> None:
|
||||
filename = "Tests/images/compression.tif"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# v2 api
|
||||
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
|
||||
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
|
||||
|
@ -167,6 +180,8 @@ class TestFileTiff:
|
|||
def test_int_resolution(self) -> None:
|
||||
filename = "Tests/images/pil168.tif"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# Try to read a file where X,Y_RESOLUTION are ints
|
||||
im.tag_v2[X_RESOLUTION] = 71
|
||||
im.tag_v2[Y_RESOLUTION] = 71
|
||||
|
@ -181,6 +196,7 @@ class TestFileTiff:
|
|||
with Image.open(
|
||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||
) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
|
||||
assert im.info["dpi"] == (dpi, dpi)
|
||||
|
||||
|
@ -198,6 +214,7 @@ class TestFileTiff:
|
|||
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
|
||||
im.save(b, format="tiff", resolution=123.45)
|
||||
with Image.open(b) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2[X_RESOLUTION] == 123.45
|
||||
assert im.tag_v2[Y_RESOLUTION] == 123.45
|
||||
|
||||
|
@ -213,10 +230,12 @@ class TestFileTiff:
|
|||
TiffImagePlugin.PREFIXES.pop()
|
||||
|
||||
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.
|
||||
with pytest.warns(UserWarning):
|
||||
i._getexif()
|
||||
im._getexif()
|
||||
|
||||
def test_save_rgba(self, tmp_path: Path) -> None:
|
||||
im = hopper("RGBA")
|
||||
|
@ -307,11 +326,13 @@ class TestFileTiff:
|
|||
)
|
||||
def test_n_frames(self, path: str, n_frames: int) -> None:
|
||||
with Image.open(path) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
def test_eoferror(self) -> None:
|
||||
with Image.open("Tests/images/multipage-lastframe.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
@ -355,19 +376,24 @@ class TestFileTiff:
|
|||
def test_frame_order(self) -> None:
|
||||
# A frame can't progress to itself after reading
|
||||
with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == 1
|
||||
|
||||
# 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:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == 2
|
||||
|
||||
# Frames don't have to be in sequence
|
||||
with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == 3
|
||||
|
||||
def test___str__(self) -> None:
|
||||
filename = "Tests/images/pil136.tiff"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# Act
|
||||
ret = str(im.ifd)
|
||||
|
||||
|
@ -378,6 +404,8 @@ class TestFileTiff:
|
|||
# Arrange
|
||||
filename = "Tests/images/pil136.tiff"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# v2 interface
|
||||
v2_tags = {
|
||||
256: 55,
|
||||
|
@ -417,6 +445,7 @@ class TestFileTiff:
|
|||
def test__delitem__(self) -> None:
|
||||
filename = "Tests/images/pil136.tiff"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
len_before = len(dict(im.ifd))
|
||||
del im.ifd[256]
|
||||
len_after = len(dict(im.ifd))
|
||||
|
@ -449,6 +478,7 @@ class TestFileTiff:
|
|||
|
||||
def test_ifd_tag_type(self) -> None:
|
||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert 0x8825 in im.tag_v2
|
||||
|
||||
def test_exif(self, tmp_path: Path) -> None:
|
||||
|
@ -537,6 +567,7 @@ class TestFileTiff:
|
|||
im = hopper(mode)
|
||||
im.save(filename, tiffinfo={262: 0})
|
||||
with Image.open(filename) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[262] == 0
|
||||
assert_image_equal(im, reloaded)
|
||||
|
||||
|
@ -615,6 +646,8 @@ class TestFileTiff:
|
|||
filename = tmp_path / "temp.tif"
|
||||
hopper("RGB").save(filename, "TIFF", **kwargs)
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# legacy interface
|
||||
assert im.tag[X_RESOLUTION][0][0] == 72
|
||||
assert im.tag[Y_RESOLUTION][0][0] == 36
|
||||
|
@ -701,6 +734,7 @@ class TestFileTiff:
|
|||
def test_planar_configuration_save(self, tmp_path: Path) -> None:
|
||||
infile = "Tests/images/tiff_tiled_planar_raw.tif"
|
||||
with Image.open(infile) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im._planar_configuration == 2
|
||||
|
||||
outfile = tmp_path / "temp.tif"
|
||||
|
@ -733,6 +767,7 @@ class TestFileTiff:
|
|||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == 3
|
||||
|
||||
# Test appending images
|
||||
|
@ -743,6 +778,7 @@ class TestFileTiff:
|
|||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as reread:
|
||||
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||
assert reread.n_frames == 3
|
||||
|
||||
# Test appending using a generator
|
||||
|
@ -754,6 +790,7 @@ class TestFileTiff:
|
|||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as reread:
|
||||
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||
assert reread.n_frames == 3
|
||||
|
||||
def test_save_all_progress(self) -> None:
|
||||
|
@ -915,6 +952,7 @@ class TestFileTiff:
|
|||
|
||||
def test_get_photoshop_blocks(self) -> None:
|
||||
with Image.open("Tests/images/lab.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert list(im.get_photoshop_blocks().keys()) == [
|
||||
1061,
|
||||
1002,
|
||||
|
|
|
@ -61,6 +61,7 @@ def test_rt_metadata(tmp_path: Path) -> None:
|
|||
img.save(f, tiffinfo=info)
|
||||
|
||||
with Image.open(f) as loaded:
|
||||
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||
assert loaded.tag[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)
|
||||
img.save(f, tiffinfo=info)
|
||||
with Image.open(f) as loaded:
|
||||
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||
|
||||
|
||||
def test_read_metadata() -> None:
|
||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||
assert {
|
||||
"YResolution": IFDRational(4294967295, 113653537),
|
||||
"PlanarConfiguration": 1,
|
||||
|
@ -128,6 +131,7 @@ def test_read_metadata() -> None:
|
|||
def test_write_metadata(tmp_path: Path) -> None:
|
||||
"""Test metadata writing through the python code"""
|
||||
with Image.open("Tests/images/hopper.tif") as img:
|
||||
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||
f = tmp_path / "temp.tiff"
|
||||
del img.tag[278]
|
||||
img.save(f, tiffinfo=img.tag)
|
||||
|
@ -135,6 +139,7 @@ def test_write_metadata(tmp_path: Path) -> None:
|
|||
original = img.tag_v2.named()
|
||||
|
||||
with Image.open(f) as loaded:
|
||||
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||
reloaded = loaded.tag_v2.named()
|
||||
|
||||
ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"]
|
||||
|
@ -165,6 +170,7 @@ def test_write_metadata(tmp_path: Path) -> None:
|
|||
def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
||||
out = tmp_path / "temp.tiff"
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
info = im.tag_v2
|
||||
del info[278]
|
||||
|
||||
|
@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
|||
im.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
|
||||
|
||||
|
||||
|
@ -231,6 +238,7 @@ def test_writing_other_types_to_ascii(
|
|||
im.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[271] == expected
|
||||
|
||||
|
||||
|
@ -248,6 +256,7 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path)
|
|||
im.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[700] == b"\x01"
|
||||
|
||||
|
||||
|
@ -267,6 +276,7 @@ def test_writing_other_types_to_undefined(
|
|||
im.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[33723] == b"1"
|
||||
|
||||
|
||||
|
@ -311,6 +321,7 @@ def test_iccprofile_binary() -> None:
|
|||
# but probably won't be able to save it.
|
||||
|
||||
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.info["icc_profile"]
|
||||
|
||||
|
@ -336,6 +347,7 @@ def test_exif_div_zero(tmp_path: Path) -> None:
|
|||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert 0 == reloaded.tag_v2[41988].numerator
|
||||
assert 0 == reloaded.tag_v2[41988].denominator
|
||||
|
||||
|
@ -355,6 +367,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
|
|||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert max_long == reloaded.tag_v2[41493].numerator
|
||||
assert 1 == reloaded.tag_v2[41493].denominator
|
||||
|
||||
|
@ -367,6 +380,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
|
|||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert max_long == reloaded.tag_v2[41493].numerator
|
||||
assert 1 == reloaded.tag_v2[41493].denominator
|
||||
|
||||
|
@ -385,6 +399,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
|||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert numerator == reloaded.tag_v2[37380].numerator
|
||||
assert denominator == reloaded.tag_v2[37380].denominator
|
||||
|
||||
|
@ -397,6 +412,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
|||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert numerator == reloaded.tag_v2[37380].numerator
|
||||
assert denominator == reloaded.tag_v2[37380].denominator
|
||||
|
||||
|
@ -410,6 +426,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
|||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
|
||||
assert -1 == reloaded.tag_v2[37380].denominator
|
||||
|
||||
|
@ -424,6 +441,7 @@ def test_ifd_signed_long(tmp_path: Path) -> None:
|
|||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[37000] == -60000
|
||||
|
||||
|
||||
|
@ -444,11 +462,13 @@ def test_empty_values() -> None:
|
|||
|
||||
def test_photoshop_info(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/issue_2278.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[34377]) == 70
|
||||
assert isinstance(im.tag_v2[34377], bytes)
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out)
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert len(reloaded.tag_v2[34377]) == 70
|
||||
assert isinstance(reloaded.tag_v2[34377], bytes)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
|||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image, features
|
||||
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -22,10 +22,12 @@ def test_n_frames() -> None:
|
|||
"""Ensure that WebP format sets n_frames and is_animated attributes correctly."""
|
||||
|
||||
with Image.open("Tests/images/hopper.webp") as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open("Tests/images/iss634.webp") as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 42
|
||||
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:
|
||||
assert isinstance(orig, GifImagePlugin.GifImageFile)
|
||||
assert orig.n_frames > 1
|
||||
|
||||
temp_file = tmp_path / "temp.webp"
|
||||
orig.save(temp_file, save_all=True)
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == orig.n_frames
|
||||
|
||||
# Compare first and last frames to the original animated GIF
|
||||
|
@ -69,6 +73,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
|||
|
||||
def check(temp_file: Path) -> None:
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 2
|
||||
|
||||
# Compare first frame to original
|
||||
|
@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 5
|
||||
assert im.is_animated
|
||||
|
||||
|
@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 5
|
||||
assert im.is_animated
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from types import ModuleType
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, WebPImagePlugin
|
||||
|
||||
from .helper import mark_if_feature_version, skip_unless_feature
|
||||
|
||||
|
@ -110,6 +110,7 @@ def test_read_no_exif() -> None:
|
|||
|
||||
test_buffer.seek(0)
|
||||
with Image.open(test_buffer) as webp_image:
|
||||
assert isinstance(webp_image, WebPImagePlugin.WebPImageFile)
|
||||
assert not webp_image._getexif()
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
|||
|
||||
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:
|
||||
|
@ -44,6 +44,15 @@ def test_load_zero_inch() -> None:
|
|||
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:
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
methodCalled = False
|
||||
|
@ -80,6 +89,7 @@ def test_load_float_dpi() -> None:
|
|||
|
||||
def test_load_set_dpi() -> None:
|
||||
with Image.open("Tests/images/drawing.wmf") as im:
|
||||
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||
assert im.size == (82, 82)
|
||||
|
||||
if hasattr(Image.core, "drawwmf"):
|
||||
|
@ -88,6 +98,22 @@ def test_load_set_dpi() -> None:
|
|||
|
||||
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"))
|
||||
def test_save(ext: str, tmp_path: Path) -> None:
|
||||
|
|
|
@ -30,6 +30,7 @@ def test_invalid_file() -> None:
|
|||
def test_load_read() -> None:
|
||||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, XpmImagePlugin.XpmImageFile)
|
||||
dummy_bytes = 1
|
||||
|
||||
# Act
|
||||
|
|
|
@ -230,10 +230,10 @@ class TestImage:
|
|||
assert_image_similar(im, reloaded, 20)
|
||||
|
||||
def test_unknown_extension(self, tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
temp_file = tmp_path / "temp.unknown"
|
||||
with pytest.raises(ValueError):
|
||||
im.save(temp_file)
|
||||
with hopper() as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.save(temp_file)
|
||||
|
||||
def test_internals(self) -> None:
|
||||
im = Image.new("L", (100, 100))
|
||||
|
@ -258,6 +258,15 @@ class TestImage:
|
|||
assert im.readonly
|
||||
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:
|
||||
im = Image.new("L", (10, 10))
|
||||
im._dump(str(tmp_path / "temp_L.ppm"))
|
||||
|
|
|
@ -1704,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None:
|
|||
BLACK,
|
||||
)
|
||||
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:
|
||||
|
|
|
@ -131,6 +131,26 @@ class TestImageFile:
|
|||
|
||||
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:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -40,8 +40,11 @@ class TestImageGrab:
|
|||
|
||||
@pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB")
|
||||
def test_grab_no_xcb(self) -> None:
|
||||
if sys.platform not in ("win32", "darwin") and not shutil.which(
|
||||
"gnome-screenshot"
|
||||
if (
|
||||
sys.platform not in ("win32", "darwin")
|
||||
and not shutil.which("gnome-screenshot")
|
||||
and not shutil.which("grim")
|
||||
and not shutil.which("spectacle")
|
||||
):
|
||||
with pytest.raises(OSError) as e:
|
||||
ImageGrab.grab()
|
||||
|
@ -57,6 +60,13 @@ class TestImageGrab:
|
|||
ImageGrab.grab(xdisplay="error.test:0.0")
|
||||
assert str(e.value).startswith("X connection failed")
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
|
||||
def test_grab_invalid_handle(self) -> None:
|
||||
with pytest.raises(OSError, match="unable to get device context for handle"):
|
||||
ImageGrab.grab(window=-1)
|
||||
with pytest.raises(OSError, match="screen grab failed"):
|
||||
ImageGrab.grab(window=0)
|
||||
|
||||
def test_grabclipboard(self) -> None:
|
||||
if sys.platform == "darwin":
|
||||
subprocess.call(["screencapture", "-cx"])
|
||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageSequence, TiffImagePlugin
|
||||
from PIL import Image, ImageSequence, PsdImagePlugin, TiffImagePlugin
|
||||
|
||||
from .helper import assert_image_equal, hopper, skip_unless_feature
|
||||
|
||||
|
@ -31,6 +31,7 @@ def test_sanity(tmp_path: Path) -> None:
|
|||
|
||||
def test_iterator() -> None:
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
i = ImageSequence.Iterator(im)
|
||||
for index in range(im.n_frames):
|
||||
assert i[index] == next(i)
|
||||
|
@ -42,6 +43,7 @@ def test_iterator() -> None:
|
|||
|
||||
def test_iterator_min_frame() -> None:
|
||||
with Image.open("Tests/images/hopper.psd") as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
i = ImageSequence.Iterator(im)
|
||||
for index in range(1, im.n_frames):
|
||||
assert i[index] == next(i)
|
||||
|
|
|
@ -81,6 +81,7 @@ def test_pickle_jpeg() -> None:
|
|||
unpickled_image = pickle.loads(pickle.dumps(image))
|
||||
|
||||
# Assert
|
||||
assert unpickled_image.filename == "Tests/images/hopper.jpg"
|
||||
assert len(unpickled_image.layer) == 3
|
||||
assert unpickled_image.layers == 3
|
||||
|
||||
|
|
112
Tests/test_pyarrow.py
Normal file
112
Tests/test_pyarrow.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any # undone
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import (
|
||||
assert_deep_equal,
|
||||
assert_image_equal,
|
||||
hopper,
|
||||
)
|
||||
|
||||
pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed")
|
||||
|
||||
TEST_IMAGE_SIZE = (10, 10)
|
||||
|
||||
|
||||
def _test_img_equals_pyarray(
|
||||
img: Image.Image, arr: Any, mask: list[int] | None
|
||||
) -> None:
|
||||
assert img.height * img.width == len(arr)
|
||||
px = img.load()
|
||||
assert px is not None
|
||||
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
||||
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
||||
if mask:
|
||||
for ix, elt in enumerate(mask):
|
||||
pixel = px[x, y]
|
||||
assert isinstance(pixel, tuple)
|
||||
assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
|
||||
else:
|
||||
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
|
||||
|
||||
|
||||
# really hard to get a non-nullable list type
|
||||
fl_uint8_4_type = pyarrow.field(
|
||||
"_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4)
|
||||
).type
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, dtype, mask",
|
||||
(
|
||||
("L", pyarrow.uint8(), None),
|
||||
("I", pyarrow.int32(), None),
|
||||
("F", pyarrow.float32(), None),
|
||||
("LA", fl_uint8_4_type, [0, 3]),
|
||||
("RGB", fl_uint8_4_type, [0, 1, 2]),
|
||||
("RGBA", fl_uint8_4_type, None),
|
||||
("RGBX", fl_uint8_4_type, None),
|
||||
("CMYK", fl_uint8_4_type, None),
|
||||
("YCbCr", fl_uint8_4_type, [0, 1, 2]),
|
||||
("HSV", fl_uint8_4_type, [0, 1, 2]),
|
||||
),
|
||||
)
|
||||
def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None:
|
||||
img = hopper(mode)
|
||||
|
||||
# Resize to non-square
|
||||
img = img.crop((3, 0, 124, 127))
|
||||
assert img.size == (121, 127)
|
||||
|
||||
arr = pyarrow.array(img)
|
||||
_test_img_equals_pyarray(img, arr, mask)
|
||||
assert arr.type == dtype
|
||||
|
||||
reloaded = Image.fromarrow(arr, mode, img.size)
|
||||
|
||||
assert reloaded
|
||||
|
||||
assert_image_equal(img, reloaded)
|
||||
|
||||
|
||||
def test_lifetime() -> None:
|
||||
# valgrind shouldn't error out here.
|
||||
# arrays should be accessible after the image is deleted.
|
||||
|
||||
img = hopper("L")
|
||||
|
||||
arr_1 = pyarrow.array(img)
|
||||
arr_2 = pyarrow.array(img)
|
||||
|
||||
del img
|
||||
|
||||
assert arr_1.sum().as_py() > 0
|
||||
del arr_1
|
||||
|
||||
assert arr_2.sum().as_py() > 0
|
||||
del arr_2
|
||||
|
||||
|
||||
def test_lifetime2() -> None:
|
||||
# valgrind shouldn't error out here.
|
||||
# img should remain after the arrays are collected.
|
||||
|
||||
img = hopper("L")
|
||||
|
||||
arr_1 = pyarrow.array(img)
|
||||
arr_2 = pyarrow.array(img)
|
||||
|
||||
assert arr_1.sum().as_py() > 0
|
||||
del arr_1
|
||||
|
||||
assert arr_2.sum().as_py() > 0
|
||||
del arr_2
|
||||
|
||||
img2 = img.copy()
|
||||
px = img2.load()
|
||||
assert px # make mypy happy
|
||||
assert isinstance(px[0, 0], int)
|
|
@ -23,5 +23,11 @@ def test_pyroma() -> None:
|
|||
)
|
||||
|
||||
else:
|
||||
# Should have a perfect score
|
||||
assert rating == (10, [])
|
||||
# Should have a perfect score, but pyroma does not support PEP 639 yet.
|
||||
assert rating == (
|
||||
9,
|
||||
[
|
||||
"Your package does neither have a license field "
|
||||
"nor any license classifiers."
|
||||
],
|
||||
)
|
||||
|
|
|
@ -39,6 +39,7 @@ class TestShellInjection:
|
|||
shutil.copy(TEST_JPG, src_file)
|
||||
|
||||
with Image.open(src_file) as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
im.load_djpeg()
|
||||
|
||||
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
|
||||
|
|
|
@ -72,4 +72,5 @@ def test_ifd_rational_save(
|
|||
im.save(out, dpi=(res, res), compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])
|
||||
|
|
64
depends/install_libavif.sh
Executable file
64
depends/install_libavif.sh
Executable file
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
version=1.2.1
|
||||
|
||||
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
|
||||
|
||||
pushd libavif-$version
|
||||
|
||||
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
|
||||
PREFIX=$(brew --prefix)
|
||||
else
|
||||
PREFIX=/usr
|
||||
fi
|
||||
|
||||
PKGCONFIG=${PKGCONFIG:-pkg-config}
|
||||
|
||||
LIBAVIF_CMAKE_FLAGS=()
|
||||
HAS_DECODER=0
|
||||
HAS_ENCODER=0
|
||||
|
||||
if $PKGCONFIG --exists aom; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
|
||||
HAS_ENCODER=1
|
||||
HAS_DECODER=1
|
||||
fi
|
||||
|
||||
if $PKGCONFIG --exists dav1d; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
|
||||
HAS_DECODER=1
|
||||
fi
|
||||
|
||||
if $PKGCONFIG --exists libgav1; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
|
||||
HAS_DECODER=1
|
||||
fi
|
||||
|
||||
if $PKGCONFIG --exists rav1e; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
|
||||
HAS_ENCODER=1
|
||||
fi
|
||||
|
||||
if $PKGCONFIG --exists SvtAv1Enc; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
|
||||
HAS_ENCODER=1
|
||||
fi
|
||||
|
||||
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
|
||||
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
|
||||
fi
|
||||
|
||||
cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=$PREFIX \
|
||||
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_MACOSX_RPATH=OFF \
|
||||
-DAVIF_LIBSHARPYUV=LOCAL \
|
||||
-DAVIF_LIBYUV=LOCAL \
|
||||
"${LIBAVIF_CMAKE_FLAGS[@]}" \
|
||||
.
|
||||
|
||||
sudo make install
|
||||
|
||||
popd
|
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
from livereload.compiler import shell
|
||||
from livereload.task import Task
|
||||
|
||||
Task.add("*.rst", shell("make html"))
|
||||
Task.add("*/*.rst", shell("make html"))
|
||||
Task.add("Makefile", shell("make html"))
|
||||
Task.add("conf.py", shell("make html"))
|
|
@ -20,8 +20,8 @@ help:
|
|||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " htmlview to open the index page built by the html target in your browser"
|
||||
@echo " htmllive to rebuild and reload HTML files in your browser"
|
||||
@echo " serve to start a local server for viewing docs"
|
||||
@echo " livehtml to start a local server for viewing docs and auto-reload on change"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
|
@ -201,9 +201,10 @@ doctest:
|
|||
htmlview: html
|
||||
$(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))"
|
||||
|
||||
.PHONY: livehtml
|
||||
livehtml: html
|
||||
livereload $(BUILDDIR)/html -p 33233
|
||||
.PHONY: htmllive
|
||||
htmllive: SPHINXBUILD = $(PYTHON) -m sphinx_autobuild
|
||||
htmllive: SPHINXOPTS = --open-browser --delay 0
|
||||
htmllive: html
|
||||
|
||||
.PHONY: serve
|
||||
serve:
|
||||
|
|
|
@ -186,7 +186,7 @@ ExifTags.IFD.Makernote
|
|||
Image.Image.get_child_images()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 11.2.0
|
||||
.. deprecated:: 11.2.1
|
||||
|
||||
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
|
||||
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
|
||||
|
|
|
@ -30,35 +30,35 @@ image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a r
|
|||
INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release
|
||||
supports the following standard modes:
|
||||
|
||||
* ``1`` (1-bit pixels, black and white, stored with one pixel per byte)
|
||||
* ``L`` (8-bit pixels, grayscale)
|
||||
* ``P`` (8-bit pixels, mapped to any other mode using a color palette)
|
||||
* ``RGB`` (3x8-bit pixels, true color)
|
||||
* ``RGBA`` (4x8-bit pixels, true color with transparency mask)
|
||||
* ``CMYK`` (4x8-bit pixels, color separation)
|
||||
* ``YCbCr`` (3x8-bit pixels, color video format)
|
||||
* ``1`` (1-bit pixels, black and white, stored with one pixel per byte)
|
||||
* ``L`` (8-bit pixels, grayscale)
|
||||
* ``P`` (8-bit pixels, mapped to any other mode using a color palette)
|
||||
* ``RGB`` (3x8-bit pixels, true color)
|
||||
* ``RGBA`` (4x8-bit pixels, true color with transparency mask)
|
||||
* ``CMYK`` (4x8-bit pixels, color separation)
|
||||
* ``YCbCr`` (3x8-bit pixels, color video format)
|
||||
|
||||
* Note that this refers to the JPEG, and not the ITU-R BT.2020, standard
|
||||
* Note that this refers to the JPEG, and not the ITU-R BT.2020, standard
|
||||
|
||||
* ``LAB`` (3x8-bit pixels, the L*a*b color space)
|
||||
* ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space)
|
||||
* ``LAB`` (3x8-bit pixels, the L*a*b color space)
|
||||
* ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space)
|
||||
|
||||
* Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees
|
||||
* Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees
|
||||
|
||||
* ``I`` (32-bit signed integer pixels)
|
||||
* ``F`` (32-bit floating point pixels)
|
||||
* ``I`` (32-bit signed integer pixels)
|
||||
* ``F`` (32-bit floating point pixels)
|
||||
|
||||
Pillow also provides limited support for a few additional modes, including:
|
||||
|
||||
* ``LA`` (L with alpha)
|
||||
* ``PA`` (P with alpha)
|
||||
* ``RGBX`` (true color with padding)
|
||||
* ``RGBa`` (true color with premultiplied alpha)
|
||||
* ``La`` (L with premultiplied alpha)
|
||||
* ``I;16`` (16-bit unsigned integer pixels)
|
||||
* ``I;16L`` (16-bit little endian unsigned integer pixels)
|
||||
* ``I;16B`` (16-bit big endian unsigned integer pixels)
|
||||
* ``I;16N`` (16-bit native endian unsigned integer pixels)
|
||||
* ``LA`` (L with alpha)
|
||||
* ``PA`` (P with alpha)
|
||||
* ``RGBX`` (true color with padding)
|
||||
* ``RGBa`` (true color with premultiplied alpha)
|
||||
* ``La`` (L with premultiplied alpha)
|
||||
* ``I;16`` (16-bit unsigned integer pixels)
|
||||
* ``I;16L`` (16-bit little endian unsigned integer pixels)
|
||||
* ``I;16B`` (16-bit big endian unsigned integer pixels)
|
||||
* ``I;16N`` (16-bit native endian unsigned integer pixels)
|
||||
|
||||
Premultiplied alpha is where the values for each other channel have been
|
||||
multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)``
|
||||
|
|
|
@ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
|
|||
Fully supported formats
|
||||
-----------------------
|
||||
|
||||
AVIF
|
||||
^^^^
|
||||
|
||||
Pillow reads and writes AVIF files, including AVIF sequence images.
|
||||
It is only possible to save 8-bit AVIF images, and all AVIF images are decoded
|
||||
as 8-bit RGB(A).
|
||||
|
||||
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||
|
||||
**quality**
|
||||
Integer, 0-100, defaults to 75. 0 gives the smallest size and poorest
|
||||
quality, 100 the largest size and best quality.
|
||||
|
||||
**subsampling**
|
||||
If present, sets the subsampling for the encoder. Defaults to ``4:2:0``.
|
||||
Options include:
|
||||
|
||||
* ``4:0:0``
|
||||
* ``4:2:0``
|
||||
* ``4:2:2``
|
||||
* ``4:4:4``
|
||||
|
||||
**speed**
|
||||
Quality/speed trade-off (0=slower/better, 10=fastest). Defaults to 6.
|
||||
|
||||
**max_threads**
|
||||
Limit the number of active threads used. By default, there is no limit. If the aom
|
||||
codec is used, there is a maximum of 64.
|
||||
|
||||
**range**
|
||||
YUV range, either "full" or "limited". Defaults to "full".
|
||||
|
||||
**codec**
|
||||
AV1 codec to use for encoding. Specific values are "aom", "rav1e", and
|
||||
"svt", presuming the chosen codec is available. Defaults to "auto", which
|
||||
will choose the first available codec in the order of the preceding list.
|
||||
|
||||
**tile_rows** / **tile_cols**
|
||||
For tile encoding, the (log 2) number of tile rows and columns to use.
|
||||
Valid values are 0-6, default 0. Ignored if "autotiling" is set to true.
|
||||
|
||||
**autotiling**
|
||||
Split the image up to allow parallelization. Enabled automatically if "tile_rows"
|
||||
and "tile_cols" both have their default values of zero.
|
||||
|
||||
**alpha_premultiplied**
|
||||
Encode the image with premultiplied alpha. Defaults to ``False``.
|
||||
|
||||
**advanced**
|
||||
Codec specific options.
|
||||
|
||||
**icc_profile**
|
||||
The ICC Profile to include in the saved file.
|
||||
|
||||
**exif**
|
||||
The exif data to include in the saved file.
|
||||
|
||||
**xmp**
|
||||
The XMP data to include in the saved file.
|
||||
|
||||
Saving sequences
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
When calling :py:meth:`~PIL.Image.Image.save` to write an AVIF file, by default
|
||||
only the first frame of a multiframe image will be saved. If the ``save_all``
|
||||
argument is present and true, then all frames will be saved, and the following
|
||||
options will also be available.
|
||||
|
||||
**append_images**
|
||||
A list of images to append as additional frames. Each of the
|
||||
images in the list can be single or multiframe images.
|
||||
|
||||
**duration**
|
||||
The display duration of each frame, in milliseconds. Pass a single
|
||||
integer for a constant duration, or a list or tuple to set the
|
||||
duration for each frame separately.
|
||||
|
||||
BLP
|
||||
^^^
|
||||
|
||||
|
@ -93,7 +170,7 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
|
|||
in ``P`` mode.
|
||||
|
||||
|
||||
.. versionadded:: 11.2.0
|
||||
.. versionadded:: 11.2.1
|
||||
DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved::
|
||||
|
||||
im.save(out, pixel_format="DXT1")
|
||||
|
@ -235,13 +312,14 @@ following options are available::
|
|||
im.save(out, save_all=True, append_images=[im1, im2, ...])
|
||||
|
||||
**save_all**
|
||||
If present and true, all frames of the image will be saved. If
|
||||
not, then only the first frame of a multiframe image will be saved.
|
||||
If present and true, or if ``append_images`` is not empty, all frames of
|
||||
the image will be saved. Otherwise, only the first frame of a multiframe
|
||||
image will be saved.
|
||||
|
||||
**append_images**
|
||||
A list of images to append as additional frames. Each of the
|
||||
images in the list can be single or multiframe images.
|
||||
This is currently supported for GIF, PDF, PNG, TIFF, and WebP.
|
||||
This is supported for AVIF, GIF, PDF, PNG, TIFF and WebP.
|
||||
|
||||
It is also supported for ICO and ICNS. If images are passed in of relevant
|
||||
sizes, they will be used instead of scaling down the main image.
|
||||
|
@ -723,8 +801,8 @@ Saving
|
|||
|
||||
When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default
|
||||
only the first frame of a multiframe image will be saved. If the ``save_all``
|
||||
argument is present and true, then all frames will be saved, and the following
|
||||
option will also be available.
|
||||
argument is present and true, or if ``append_images`` is not empty, all frames
|
||||
will be saved.
|
||||
|
||||
**append_images**
|
||||
A list of images to append as additional pictures. Each of the
|
||||
|
@ -934,7 +1012,8 @@ Saving
|
|||
|
||||
When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file
|
||||
will be saved. To save an APNG file (including a single frame APNG), the ``save_all``
|
||||
parameter must be set to ``True``. The following parameters can also be set:
|
||||
parameter should be set to ``True`` or ``append_images`` should not be empty. The
|
||||
following parameters can also be set:
|
||||
|
||||
**default_image**
|
||||
Boolean value, specifying whether or not the base image is a default image.
|
||||
|
@ -1163,7 +1242,8 @@ Saving
|
|||
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
|
||||
|
||||
**save_all**
|
||||
If true, Pillow will save all frames of the image to a multiframe tiff document.
|
||||
If true, or if ``append_images`` is not empty, Pillow will save all frames of the
|
||||
image to a multiframe tiff document.
|
||||
|
||||
.. versionadded:: 3.4.0
|
||||
|
||||
|
@ -1313,8 +1393,8 @@ Saving sequences
|
|||
|
||||
When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default
|
||||
only the first frame of a multiframe image will be saved. If the ``save_all``
|
||||
argument is present and true, then all frames will be saved, and the following
|
||||
options will also be available.
|
||||
argument is present and true, or if ``append_images`` is not empty, all frames
|
||||
will be saved, and the following options will also be available.
|
||||
|
||||
**append_images**
|
||||
A list of images to append as additional frames. Each of the
|
||||
|
@ -1584,6 +1664,11 @@ The :py:meth:`~PIL.Image.open` method sets the following
|
|||
Transparency color index. This key is omitted if the image is not
|
||||
transparent.
|
||||
|
||||
XV Thumbnails
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
Pillow can read XV thumbnail files.
|
||||
|
||||
Write-only formats
|
||||
------------------
|
||||
|
||||
|
@ -1616,15 +1701,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
|
|||
**save_all**
|
||||
If a multiframe image is used, by default, only the first image will be saved.
|
||||
To save all frames, each frame to a separate page of the PDF, the ``save_all``
|
||||
parameter must be present and set to ``True``.
|
||||
parameter should be present and set to ``True`` or ``append_images`` should not be
|
||||
empty.
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
|
||||
**append_images**
|
||||
A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each
|
||||
of the images in the list can be single or multiframe images. The ``save_all``
|
||||
parameter must be present and set to ``True`` in conjunction with
|
||||
``append_images``.
|
||||
of the images in the list can be single or multiframe images.
|
||||
|
||||
.. versionadded:: 4.2.0
|
||||
|
||||
|
@ -1690,11 +1774,6 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
|
|||
|
||||
.. versionadded:: 5.3.0
|
||||
|
||||
XV Thumbnails
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
Pillow can read XV thumbnail files.
|
||||
|
||||
Identify-only formats
|
||||
---------------------
|
||||
|
||||
|
|
|
@ -534,7 +534,6 @@ You can create animated GIFs with Pillow, e.g.
|
|||
# Save the images as an animated GIF
|
||||
images[0].save(
|
||||
"animated_hopper.gif",
|
||||
save_all=True,
|
||||
append_images=images[1:],
|
||||
duration=500, # duration of each frame in milliseconds
|
||||
loop=0, # loop forever
|
||||
|
|
|
@ -89,6 +89,14 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **libxcb** provides X11 screengrab support.
|
||||
|
||||
* **libavif** provides support for the AVIF format.
|
||||
|
||||
* Pillow requires libavif version **1.0.0** or greater.
|
||||
* libavif is merely an API that wraps AVIF codecs. If you are compiling
|
||||
libavif from source, you will also need to install both an AVIF encoder
|
||||
and decoder, such as rav1e and dav1d, or libaom, which both encodes and
|
||||
decodes AVIF images.
|
||||
|
||||
.. tab:: Linux
|
||||
|
||||
If you didn't build Python from source, make sure you have Python's
|
||||
|
@ -117,6 +125,12 @@ Many of Pillow's features require external libraries:
|
|||
To install libraqm, ``sudo apt-get install meson`` and then see
|
||||
``depends/install_raqm.sh``.
|
||||
|
||||
Build prerequisites for libavif on Ubuntu are installed with::
|
||||
|
||||
sudo apt-get install cmake ninja-build nasm
|
||||
|
||||
Then see ``depends/install_libavif.sh`` to build and install libavif.
|
||||
|
||||
Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
|
||||
|
||||
sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
|
||||
|
@ -148,7 +162,15 @@ Many of Pillow's features require external libraries:
|
|||
The easiest way to install external libraries is via `Homebrew
|
||||
<https://brew.sh/>`_. After you install Homebrew, run::
|
||||
|
||||
brew install libjpeg libraqm libtiff little-cms2 openjpeg webp
|
||||
brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp
|
||||
|
||||
If you would like to use libavif with more codecs than just aom, then
|
||||
instead of installing libavif through Homebrew directly, you can use
|
||||
Homebrew to install libavif's build dependencies::
|
||||
|
||||
brew install aom dav1d rav1e svt-av1
|
||||
|
||||
Then see ``depends/install_libavif.sh`` to install libavif.
|
||||
|
||||
.. tab:: Windows
|
||||
|
||||
|
@ -187,7 +209,8 @@ Many of Pillow's features require external libraries:
|
|||
mingw-w64-x86_64-libwebp \
|
||||
mingw-w64-x86_64-openjpeg2 \
|
||||
mingw-w64-x86_64-libimagequant \
|
||||
mingw-w64-x86_64-libraqm
|
||||
mingw-w64-x86_64-libraqm \
|
||||
mingw-w64-x86_64-libavif
|
||||
|
||||
.. tab:: FreeBSD
|
||||
|
||||
|
@ -199,7 +222,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
Prerequisites are installed on **FreeBSD 10 or 11** with::
|
||||
|
||||
sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
|
||||
sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif
|
||||
|
||||
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
|
||||
|
||||
|
@ -261,14 +284,16 @@ Build Options
|
|||
* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
|
||||
``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
|
||||
``-C lcms=disable``, ``-C webp=disable``,
|
||||
``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``.
|
||||
``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``,
|
||||
``-C avif=disable``.
|
||||
Disable building the corresponding feature even if the development
|
||||
libraries are present on the building machine.
|
||||
|
||||
* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``,
|
||||
``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``,
|
||||
``-C lcms=enable``, ``-C webp=enable``,
|
||||
``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``.
|
||||
``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``,
|
||||
``-C avif=enable``.
|
||||
Require that the corresponding feature is built. The build will raise
|
||||
an exception if the libraries are not found. Tcl and Tk must be used
|
||||
together.
|
||||
|
|
|
@ -23,7 +23,7 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| Amazon Linux 2023 | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Arch | 3.12 | x86-64 |
|
||||
| Arch | 3.13 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| CentOS Stream 9 | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
|
@ -31,10 +31,10 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 40 | 3.12 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 41 | 3.13 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 42 | 3.13 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Gentoo | 3.12 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| macOS 13 Ventura | 3.9 | x86-64 |
|
||||
|
@ -73,7 +73,7 @@ These platforms have been reported to work at the versions mentioned.
|
|||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+============================+==================+==============+
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.2.1 |arm |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.8 | 10.4.0 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
|
|
|
@ -79,6 +79,7 @@ Constructing images
|
|||
|
||||
.. autofunction:: new
|
||||
.. autofunction:: fromarray
|
||||
.. autofunction:: fromarrow
|
||||
.. autofunction:: frombytes
|
||||
.. autofunction:: frombuffer
|
||||
|
||||
|
@ -370,6 +371,8 @@ Protocols
|
|||
|
||||
.. autoclass:: SupportsArrayInterface
|
||||
:show-inheritance:
|
||||
.. autoclass:: SupportsArrowArrayInterface
|
||||
:show-inheritance:
|
||||
.. autoclass:: SupportsGetData
|
||||
:show-inheritance:
|
||||
|
||||
|
|
|
@ -286,6 +286,14 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
|
||||
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: media_white_point
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
This tag specifies the media white point and is used for
|
||||
generating absolute colorimetry.
|
||||
|
||||
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: media_white_point_temperature
|
||||
:type: float | None
|
||||
|
||||
|
|
|
@ -391,7 +391,7 @@ Methods
|
|||
the relative alignment of lines. Use the ``anchor`` parameter to
|
||||
specify the alignment to ``xy``.
|
||||
|
||||
.. versionadded:: 11.2.0 ``"justify"``
|
||||
.. versionadded:: 11.2.1 ``"justify"``
|
||||
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||
Requires libraqm.
|
||||
|
@ -462,7 +462,7 @@ Methods
|
|||
the relative alignment of lines. Use the ``anchor`` parameter to
|
||||
specify the alignment to ``xy``.
|
||||
|
||||
.. versionadded:: 11.2.0 ``"justify"``
|
||||
.. versionadded:: 11.2.1 ``"justify"``
|
||||
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||
Requires libraqm.
|
||||
|
@ -609,7 +609,7 @@ Methods
|
|||
the relative alignment of lines. Use the ``anchor`` parameter to
|
||||
specify the alignment to ``xy``.
|
||||
|
||||
.. versionadded:: 11.2.0 ``"justify"``
|
||||
.. versionadded:: 11.2.1 ``"justify"``
|
||||
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||
Requires libraqm.
|
||||
|
@ -663,7 +663,7 @@ Methods
|
|||
the relative alignment of lines. Use the ``anchor`` parameter to
|
||||
specify the alignment to ``xy``.
|
||||
|
||||
.. versionadded:: 11.2.0 ``"justify"``
|
||||
.. versionadded:: 11.2.1 ``"justify"``
|
||||
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||
Requires libraqm.
|
||||
|
|
|
@ -9,15 +9,16 @@ or the clipboard to a PIL image memory.
|
|||
|
||||
.. versionadded:: 1.1.3
|
||||
|
||||
.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None)
|
||||
.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None, window=None)
|
||||
|
||||
Take a snapshot of the screen. The pixels inside the bounding box are returned as
|
||||
an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted,
|
||||
the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen.
|
||||
|
||||
On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return
|
||||
a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is
|
||||
installed. To disable this behaviour, pass ``xdisplay=""`` instead.
|
||||
a snapshot of the screen, ``gnome-screenshot``, ``grim`` or ``spectacle`` will be
|
||||
used as a fallback if they are installed. To disable this behaviour, pass
|
||||
``xdisplay=""`` instead.
|
||||
|
||||
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux)
|
||||
|
||||
|
@ -39,6 +40,11 @@ or the clipboard to a PIL image memory.
|
|||
You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``.
|
||||
|
||||
.. versionadded:: 7.1.0
|
||||
|
||||
:param window:
|
||||
HWND, to capture a single window. Windows only.
|
||||
|
||||
.. versionadded:: 11.2.1
|
||||
:return: An image
|
||||
|
||||
.. py:function:: grabclipboard()
|
||||
|
|
88
docs/reference/arrow_support.rst
Normal file
88
docs/reference/arrow_support.rst
Normal file
|
@ -0,0 +1,88 @@
|
|||
.. _arrow-support:
|
||||
|
||||
=============
|
||||
Arrow Support
|
||||
=============
|
||||
|
||||
`Arrow <https://arrow.apache.org/>`__
|
||||
is an in-memory data exchange format that is the spiritual
|
||||
successor to the NumPy array interface. It provides for zero-copy
|
||||
access to columnar data, which in our case is ``Image`` data.
|
||||
|
||||
The goal with Arrow is to provide native zero-copy interoperability
|
||||
with any Arrow provider or consumer in the Python ecosystem.
|
||||
|
||||
.. warning:: Zero-copy does not mean zero allocation -- the internal
|
||||
memory layout of Pillow images contains an allocation for row
|
||||
pointers, so there is a non-zero, but significantly smaller than a
|
||||
full-copy memory cost to reading an Arrow image.
|
||||
|
||||
|
||||
Data Formats
|
||||
============
|
||||
|
||||
Pillow currently supports exporting Arrow images in all modes
|
||||
**except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to
|
||||
line-length packing in these modes making for non-continuous memory.
|
||||
|
||||
For single-band images, the exported array is width*height elements,
|
||||
with each pixel corresponding to the appropriate Arrow type.
|
||||
|
||||
For multiband images, the exported array is width*height fixed-length
|
||||
four-element arrays of uint8. This is memory compatible with the raw
|
||||
image storage of four bytes per pixel.
|
||||
|
||||
Mode ``1`` images are exported as one uint8 byte/pixel, as this is
|
||||
consistent with the internal storage.
|
||||
|
||||
Pillow will accept, but not produce, one other format. For any
|
||||
multichannel image with 32-bit storage per pixel, Pillow will accept
|
||||
an array of width*height int32 elements, which will then be
|
||||
interpreted using the mode-specific interpretation of the bytes.
|
||||
|
||||
The image mode must match the Arrow band format when reading single
|
||||
channel images.
|
||||
|
||||
Memory Allocator
|
||||
================
|
||||
|
||||
Pillow's default memory allocator, the :ref:`block_allocator`,
|
||||
allocates up to a 16 MB block for images by default. Larger images
|
||||
overflow into additional blocks. Arrow requires a single continuous
|
||||
memory allocation, so images allocated in multiple blocks cannot be
|
||||
exported in the Arrow format.
|
||||
|
||||
To enable the single block allocator::
|
||||
|
||||
from PIL import Image
|
||||
Image.core.set_use_block_allocator(1)
|
||||
|
||||
Note that this is a global setting, not a per-image setting.
|
||||
|
||||
Unsupported Features
|
||||
====================
|
||||
|
||||
* Table/dataframe protocol. We support a single array.
|
||||
* Null markers, producing or consuming. Null values are inferred from
|
||||
the mode, e.g. RGB images are stored in the first three bytes of
|
||||
each 32-bit pixel, and the last byte is an implied null.
|
||||
* Schema negotiation. There is an optional schema for the requested
|
||||
datatype in the Arrow source interface. We ignore that
|
||||
parameter.
|
||||
* Array metadata.
|
||||
|
||||
Internal Details
|
||||
================
|
||||
|
||||
Python Arrow C interface:
|
||||
https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
|
||||
|
||||
The memory that is exported from the Arrow interface is shared -- not
|
||||
copied, so the lifetime of the memory allocation is no longer strictly
|
||||
tied to the life of the Python object.
|
||||
|
||||
The core imaging struct now has a refcount associated with it, and the
|
||||
lifetime of the core image struct is now divorced from the Python
|
||||
image object. Creating an arrow reference to the image increments the
|
||||
refcount, and the imaging struct is only released when the refcount
|
||||
reaches zero.
|
|
@ -1,3 +1,6 @@
|
|||
|
||||
.. _block_allocator:
|
||||
|
||||
Block Allocator
|
||||
===============
|
||||
|
||||
|
@ -34,14 +37,14 @@ fresh allocation. This caching of free blocks is currently disabled by
|
|||
default, but can be enabled and tweaked using three environment
|
||||
variables:
|
||||
|
||||
* ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory
|
||||
allocations. Valid values are powers of 2 between 1 and
|
||||
128, inclusive. Defaults to 1.
|
||||
* ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory
|
||||
allocations. Valid values are powers of 2 between 1 and
|
||||
128, inclusive. Defaults to 1.
|
||||
|
||||
* ``PILLOW_BLOCK_SIZE``, in bytes, K, or M. Specifies the maximum
|
||||
block size for ``ImagingAllocateArray``. Valid values are
|
||||
integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M.
|
||||
* ``PILLOW_BLOCK_SIZE``, in bytes, K, or M. Specifies the maximum
|
||||
block size for ``ImagingAllocateArray``. Valid values are
|
||||
integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M.
|
||||
|
||||
* ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to
|
||||
retain to fill future memory requests. Any freed blocks over this
|
||||
threshold will be returned to the OS immediately. Defaults to 0.
|
||||
* ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to
|
||||
retain to fill future memory requests. Any freed blocks over this
|
||||
threshold will be returned to the OS immediately. Defaults to 0.
|
||||
|
|
|
@ -21,6 +21,7 @@ Support for the following modules can be checked:
|
|||
* ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`.
|
||||
* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`.
|
||||
* ``webp``: WebP image support.
|
||||
* ``avif``: AVIF image support.
|
||||
|
||||
.. autofunction:: PIL.features.check_module
|
||||
.. autofunction:: PIL.features.version_module
|
||||
|
|
|
@ -9,3 +9,4 @@ Internal Reference
|
|||
block_allocator
|
||||
internal_modules
|
||||
c_extension_debugging
|
||||
arrow_support
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
Plugin reference
|
||||
================
|
||||
|
||||
:mod:`~PIL.AvifImagePlugin` Module
|
||||
----------------------------------
|
||||
|
||||
.. automodule:: PIL.AvifImagePlugin
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`~PIL.BmpImagePlugin` Module
|
||||
---------------------------------
|
||||
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
11.2.0
|
||||
------
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
:cve:`YYYY-XXXXX`: TODO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
Image.Image.get_child_images()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 11.2.0
|
||||
|
||||
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
|
||||
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
|
||||
method uses an image's file pointer, and so child images could only be retrieved from
|
||||
an :py:class:`PIL.ImageFile.ImageFile` instance.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
``"justify"`` multiline text alignment
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be
|
||||
aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`::
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
im = Image.new("RGB", (50, 25))
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify")
|
||||
draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify")
|
||||
|
||||
Check for MozJPEG
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can check if Pillow has been built against the MozJPEG version of the
|
||||
libjpeg library, and what version of MozJPEG is being used::
|
||||
|
||||
from PIL import features
|
||||
features.check_feature("mozjpeg") # True or False
|
||||
features.version_feature("mozjpeg") # "4.1.1" for example, or None
|
||||
|
||||
Saving compressed DDS images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3,
|
||||
DXT5, BC2, BC3 and BC5 are supported::
|
||||
|
||||
im.save("out.dds", pixel_format="DXT1")
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
118
docs/releasenotes/11.2.1.rst
Normal file
118
docs/releasenotes/11.2.1.rst
Normal file
|
@ -0,0 +1,118 @@
|
|||
11.2.1
|
||||
------
|
||||
|
||||
.. warning::
|
||||
|
||||
The release of Pillow *11.2.0* was halted prematurely, due to hitting PyPI's
|
||||
project size limit and concern over the size of Pillow wheels containing libavif.
|
||||
The PyPI limit has now been increased and Pillow *11.2.1* has been released
|
||||
instead, without libavif included in the wheels.
|
||||
To avoid confusion, the incomplete 11.2.0 release has been removed from PyPI.
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
Undefined shift when loading compressed DDS images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When loading some compressed DDS formats, an integer was bitshifted by 24 places to
|
||||
generate the 32 bits of the lookup table. This was undefined behaviour, and has been
|
||||
present since Pillow 3.4.0.
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
Image.Image.get_child_images()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 11.2.1
|
||||
|
||||
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
|
||||
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
|
||||
method uses an image's file pointer, and so child images could only be retrieved from
|
||||
an :py:class:`PIL.ImageFile.ImageFile` instance.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
``append_images`` no longer requires ``save_all``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Previously, ``save_all`` was required to in order to use ``append_images``. Now,
|
||||
``save_all`` will default to ``True`` if ``append_images`` is not empty and the format
|
||||
supports saving multiple frames::
|
||||
|
||||
im.save("out.gif", append_images=ims)
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
``"justify"`` multiline text alignment
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be
|
||||
aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`::
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
im = Image.new("RGB", (50, 25))
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify")
|
||||
draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify")
|
||||
|
||||
Specify window in ImageGrab on Windows
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the
|
||||
HWND::
|
||||
|
||||
from PIL import ImageGrab
|
||||
ImageGrab.grab(window=hwnd)
|
||||
|
||||
Check for MozJPEG
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can check if Pillow has been built against the MozJPEG version of the
|
||||
libjpeg library, and what version of MozJPEG is being used::
|
||||
|
||||
from PIL import features
|
||||
features.check_feature("mozjpeg") # True or False
|
||||
features.version_feature("mozjpeg") # "4.1.1" for example, or None
|
||||
|
||||
Saving compressed DDS images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3,
|
||||
DXT5, BC2, BC3 and BC5 are supported::
|
||||
|
||||
im.save("out.dds", pixel_format="DXT1")
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
Arrow support
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
`Arrow <https://arrow.apache.org/>`__ is an in-memory data exchange format that is the
|
||||
spiritual successor to the NumPy array interface. It provides for zero-copy access to
|
||||
columnar data, which in our case is ``Image`` data.
|
||||
|
||||
To create an image with zero-copy shared memory from an object exporting the
|
||||
arrow_c_array interface protocol::
|
||||
|
||||
from PIL import Image
|
||||
import pyarrow as pa
|
||||
arr = pa.array([0]*(5*5*4), type=pa.uint8())
|
||||
im = Image.fromarrow(arr, 'RGBA', (5, 5))
|
||||
|
||||
Pillow images can also be converted to Arrow objects::
|
||||
|
||||
from PIL import Image
|
||||
import pyarrow as pa
|
||||
im = Image.open('hopper.jpg')
|
||||
arr = pa.array(im)
|
||||
|
||||
Reading and writing AVIF images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Pillow can now read and write AVIF images when built from source with libavif 1.0.0
|
||||
or later.
|
|
@ -14,7 +14,7 @@ expected to be backported to earlier versions.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
11.2.0
|
||||
11.2.1
|
||||
11.1.0
|
||||
11.0.0
|
||||
10.4.0
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[build-system]
|
||||
build-backend = "backend"
|
||||
requires = [
|
||||
"setuptools>=67.8",
|
||||
"setuptools>=77",
|
||||
]
|
||||
backend-path = [
|
||||
"_custom_build",
|
||||
|
@ -14,14 +14,14 @@ readme = "README.md"
|
|||
keywords = [
|
||||
"Imaging",
|
||||
]
|
||||
license = { text = "MIT-CMU" }
|
||||
license = "MIT-CMU"
|
||||
license-files = [ "LICENSE" ]
|
||||
authors = [
|
||||
{ name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
|
||||
]
|
||||
requires-python = ">=3.9"
|
||||
classifiers = [
|
||||
"Development Status :: 6 - Mature",
|
||||
"License :: OSI Approved :: CMU License (MIT-CMU)",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
|
@ -44,6 +44,7 @@ optional-dependencies.docs = [
|
|||
"furo",
|
||||
"olefile",
|
||||
"sphinx>=8.2",
|
||||
"sphinx-autobuild",
|
||||
"sphinx-copybutton",
|
||||
"sphinx-inline-tabs",
|
||||
"sphinxext-opengraph",
|
||||
|
@ -54,6 +55,10 @@ optional-dependencies.fpx = [
|
|||
optional-dependencies.mic = [
|
||||
"olefile",
|
||||
]
|
||||
optional-dependencies.test-arrow = [
|
||||
"pyarrow",
|
||||
]
|
||||
|
||||
optional-dependencies.tests = [
|
||||
"check-manifest",
|
||||
"coverage>=7.4.2",
|
||||
|
@ -67,6 +72,7 @@ optional-dependencies.tests = [
|
|||
"pytest-timeout",
|
||||
"trove-classifiers>=2024.10.12",
|
||||
]
|
||||
|
||||
optional-dependencies.typing = [
|
||||
"typing-extensions; python_version<'3.10'",
|
||||
]
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user