diff --git a/.ci/install.sh b/.ci/install.sh
index e61752750..d065e7ab5 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -20,10 +20,10 @@ fi
set -e
if [[ $(uname) != CYGWIN* ]]; then
- sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
+ sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
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
diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt
index 2fd3eb6ff..db5f89c9a 100644
--- a/.ci/requirements-cibw.txt
+++ b/.ci/requirements-cibw.txt
@@ -1 +1 @@
-cibuildwheel==2.23.0
+cibuildwheel==2.23.2
diff --git a/.clang-format b/.clang-format
index 143dde82c..1871d1f7a 100644
--- a/.clang-format
+++ b/.clang-format
@@ -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
diff --git a/.github/renovate.json b/.github/renovate.json
index f48b670ec..91fa0426f 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -16,6 +16,6 @@
}
],
"schedule": [
- "on the 3rd day of the month"
+ "* * 3 * *"
]
}
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index 6aa59a4ac..94e3d5d08 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -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
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index da5e191da..0b90732eb 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -35,6 +35,10 @@ jobs:
matrix:
os: ["ubuntu-latest"]
docker: [
+ # Run slower jobs first to give them a headstart and reduce waiting time
+ ubuntu-24.04-noble-ppc64le,
+ ubuntu-24.04-noble-s390x,
+ # Then run the remainder
alpine,
amazon-2-amd64,
amazon-2023-amd64,
@@ -43,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,
@@ -52,13 +56,9 @@ jobs:
dockerTag: [main]
include:
- docker: "ubuntu-24.04-noble-ppc64le"
- os: "ubuntu-22.04"
qemu-arch: "ppc64le"
- dockerTag: main
- docker: "ubuntu-24.04-noble-s390x"
- os: "ubuntu-22.04"
qemu-arch: "s390x"
- dockerTag: main
- docker: "ubuntu-24.04-noble-arm64v8"
os: "ubuntu-24.04-arm"
dockerTag: main
@@ -75,8 +75,9 @@ jobs:
- name: Set up QEMU
if: "matrix.qemu-arch"
- run: |
- docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }}
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: ${{ matrix.qemu-arch }}
- name: Docker pull
run: |
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index bb6d7dc37..5a83c16c3 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -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 \
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index ef49ff332..bf8ec2f2c 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -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,14 +88,18 @@ 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: |
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
- choco install ghostscript --version=10.4.0 --no-progress
- echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
+ choco install ghostscript --version=10.5.0 --no-progress
+ echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
@@ -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'
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c4ad88be9..006d574f3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -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
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 202a8935d..a4592871f 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -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
-TIFF_VERSION=4.6.0
+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
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 1fe6badae..33e1976f0 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -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 }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5ff947d41..140ce33be 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -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]
diff --git a/Makefile b/Makefile
index 53164b08a..5a8152454 100644
--- a/Makefile
+++ b/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"
diff --git a/Tests/README.rst b/Tests/README.rst
index 2d014e5a4..a955ec4fa 100644
--- a/Tests/README.rst
+++ b/Tests/README.rst
@@ -1,4 +1,4 @@
-Pillow Tests
+Pillow tests
============
Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``.
diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py
index dbdd5a4f5..58566c4b2 100644
--- a/Tests/check_j2k_overflow.py
+++ b/Tests/check_j2k_overflow.py
@@ -9,6 +9,6 @@ from PIL import Image
def test_j2k_overflow(tmp_path: Path) -> None:
im = Image.new("RGBA", (1024, 131584))
- target = str(tmp_path / "temp.jpc")
+ target = tmp_path / "temp.jpc"
with pytest.raises(OSError):
im.save(target)
diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py
index a9ce79e57..c9feda3b1 100644
--- a/Tests/check_large_memory.py
+++ b/Tests/check_large_memory.py
@@ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
- f = str(tmp_path / "temp.png")
+ f = tmp_path / "temp.png"
im = Image.new("L", (xdim, ydim), 0)
im.save(f)
diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py
index f4ca8d0aa..458b0ab72 100644
--- a/Tests/check_large_memory_numpy.py
+++ b/Tests/check_large_memory_numpy.py
@@ -28,7 +28,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype)
- f = str(tmp_path / "temp.png")
+ f = tmp_path / "temp.png"
im = Image.fromarray(a, "L")
im.save(f)
diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py
index 563be0b74..8ba40ba3f 100644
--- a/Tests/check_wheel.py
+++ b/Tests/check_wheel.py
@@ -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
diff --git a/Tests/helper.py b/Tests/helper.py
index 764935f87..909fff879 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -13,6 +13,7 @@ import tempfile
from collections.abc import Sequence
from functools import lru_cache
from io import BytesIO
+from pathlib import Path
from typing import Any, Callable
import pytest
@@ -95,7 +96,10 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -
def assert_image_equal_tofile(
- a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
+ a: Image.Image,
+ filename: str | Path,
+ msg: str | None = None,
+ mode: str | None = None,
) -> None:
with Image.open(filename) as img:
if mode:
@@ -136,7 +140,7 @@ def assert_image_similar(
def assert_image_similar_tofile(
a: Image.Image,
- filename: str,
+ filename: str | Path,
epsilon: float,
msg: str | None = None,
) -> None:
diff --git a/Tests/images/avif/exif.avif b/Tests/images/avif/exif.avif
new file mode 100644
index 000000000..07964487f
Binary files /dev/null and b/Tests/images/avif/exif.avif differ
diff --git a/Tests/images/avif/hopper-missing-pixi.avif b/Tests/images/avif/hopper-missing-pixi.avif
new file mode 100644
index 000000000..c793ade1c
Binary files /dev/null and b/Tests/images/avif/hopper-missing-pixi.avif differ
diff --git a/Tests/images/avif/hopper.avif b/Tests/images/avif/hopper.avif
new file mode 100644
index 000000000..87e4394f0
Binary files /dev/null and b/Tests/images/avif/hopper.avif differ
diff --git a/Tests/images/avif/hopper.heif b/Tests/images/avif/hopper.heif
new file mode 100644
index 000000000..f6dbd1b47
Binary files /dev/null and b/Tests/images/avif/hopper.heif differ
diff --git a/Tests/images/avif/hopper_avif_write.png b/Tests/images/avif/hopper_avif_write.png
new file mode 100644
index 000000000..a47a0562b
Binary files /dev/null and b/Tests/images/avif/hopper_avif_write.png differ
diff --git a/Tests/images/avif/icc_profile.avif b/Tests/images/avif/icc_profile.avif
new file mode 100644
index 000000000..658cfec17
Binary files /dev/null and b/Tests/images/avif/icc_profile.avif differ
diff --git a/Tests/images/avif/icc_profile_none.avif b/Tests/images/avif/icc_profile_none.avif
new file mode 100644
index 000000000..c73e70a3a
Binary files /dev/null and b/Tests/images/avif/icc_profile_none.avif differ
diff --git a/Tests/images/avif/rot0mir0.avif b/Tests/images/avif/rot0mir0.avif
new file mode 100644
index 000000000..f57203093
Binary files /dev/null and b/Tests/images/avif/rot0mir0.avif differ
diff --git a/Tests/images/avif/rot0mir1.avif b/Tests/images/avif/rot0mir1.avif
new file mode 100644
index 000000000..8a9fb5e54
Binary files /dev/null and b/Tests/images/avif/rot0mir1.avif differ
diff --git a/Tests/images/avif/rot1mir0.avif b/Tests/images/avif/rot1mir0.avif
new file mode 100644
index 000000000..0c7c7620f
Binary files /dev/null and b/Tests/images/avif/rot1mir0.avif differ
diff --git a/Tests/images/avif/rot1mir1.avif b/Tests/images/avif/rot1mir1.avif
new file mode 100644
index 000000000..0181b088c
Binary files /dev/null and b/Tests/images/avif/rot1mir1.avif differ
diff --git a/Tests/images/avif/rot2mir0.avif b/Tests/images/avif/rot2mir0.avif
new file mode 100644
index 000000000..ddaa02f3f
Binary files /dev/null and b/Tests/images/avif/rot2mir0.avif differ
diff --git a/Tests/images/avif/rot2mir1.avif b/Tests/images/avif/rot2mir1.avif
new file mode 100644
index 000000000..63326b86c
Binary files /dev/null and b/Tests/images/avif/rot2mir1.avif differ
diff --git a/Tests/images/avif/rot3mir0.avif b/Tests/images/avif/rot3mir0.avif
new file mode 100644
index 000000000..ed5dbe3d0
Binary files /dev/null and b/Tests/images/avif/rot3mir0.avif differ
diff --git a/Tests/images/avif/rot3mir1.avif b/Tests/images/avif/rot3mir1.avif
new file mode 100644
index 000000000..ccd8861d5
Binary files /dev/null and b/Tests/images/avif/rot3mir1.avif differ
diff --git a/Tests/images/avif/star.avifs b/Tests/images/avif/star.avifs
new file mode 100644
index 000000000..f2753395f
Binary files /dev/null and b/Tests/images/avif/star.avifs differ
diff --git a/Tests/images/avif/star.gif b/Tests/images/avif/star.gif
new file mode 100644
index 000000000..52076cafd
Binary files /dev/null and b/Tests/images/avif/star.gif differ
diff --git a/Tests/images/avif/star.png b/Tests/images/avif/star.png
new file mode 100644
index 000000000..468dcde00
Binary files /dev/null and b/Tests/images/avif/star.png differ
diff --git a/Tests/images/avif/transparency.avif b/Tests/images/avif/transparency.avif
new file mode 100644
index 000000000..f808357fc
Binary files /dev/null and b/Tests/images/avif/transparency.avif differ
diff --git a/Tests/images/avif/xmp_tags_orientation.avif b/Tests/images/avif/xmp_tags_orientation.avif
new file mode 100644
index 000000000..41faa6050
Binary files /dev/null and b/Tests/images/avif/xmp_tags_orientation.avif differ
diff --git a/Tests/images/drawing_emf_ref_72_144.png b/Tests/images/drawing_emf_ref_72_144.png
new file mode 100644
index 000000000..000377b6c
Binary files /dev/null and b/Tests/images/drawing_emf_ref_72_144.png differ
diff --git a/Tests/images/full_gimp_palette.gpl b/Tests/images/full_gimp_palette.gpl
new file mode 100644
index 000000000..004217210
--- /dev/null
+++ b/Tests/images/full_gimp_palette.gpl
@@ -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
diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png
index 1b58889c8..8992a1657 100644
Binary files a/Tests/images/imagedraw/discontiguous_corners_polygon.png and b/Tests/images/imagedraw/discontiguous_corners_polygon.png differ
diff --git a/Tests/test_arrow.py b/Tests/test_arrow.py
new file mode 100644
index 000000000..b86c77b9a
--- /dev/null
+++ b/Tests/test_arrow.py
@@ -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)
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index 9d5154fca..a5734c202 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -12,6 +12,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"
@@ -20,6 +21,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"
@@ -34,8 +36,11 @@ def test_apng_basic() -> None:
with pytest.raises(EOFError):
im.seek(2)
- # test rewind support
im.seek(0)
+ with pytest.raises(ValueError, match="cannot seek to frame 2"):
+ im._seek(2)
+
+ # test rewind support
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
assert im.getpixel((64, 32)) == (255, 0, 0, 255)
im.seek(1)
@@ -49,6 +54,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)
@@ -56,31 +62,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)
@@ -88,21 +100,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)
@@ -129,6 +145,7 @@ def test_apng_dispose_op_previous_frame() -> None:
# ],
# )
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
+ assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
@@ -142,26 +159,31 @@ def test_apng_dispose_op_background_p_mode() -> None:
def test_apng_blend() -> None:
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)
@@ -175,6 +197,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)
@@ -230,24 +253,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")
@@ -255,6 +282,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")
@@ -262,6 +290,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")
@@ -271,25 +300,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)
@@ -297,26 +332,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()
@@ -336,16 +376,18 @@ 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()
def test_apng_save(tmp_path: Path) -> None:
with Image.open("Tests/images/apng/single_frame.png") as im:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im.save(test_file, save_all=True)
with Image.open(test_file) as im:
+ assert isinstance(im, PngImagePlugin.PngImageFile)
im.load()
assert not im.is_animated
assert im.n_frames == 1
@@ -361,6 +403,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
@@ -372,7 +415,7 @@ def test_apng_save(tmp_path: Path) -> None:
def test_apng_save_alpha(tmp_path: Path) -> None:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127))
@@ -390,7 +433,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
# frames with image data spanning multiple fdAT chunks (in this case
# both the default image and first animation frame will span multiple
# data chunks)
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
with Image.open("Tests/images/old-style-jpeg-compression.png") as im:
frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))]
im.save(
@@ -400,12 +443,13 @@ 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()
def test_apng_save_duration_loop(tmp_path: Path) -> None:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
with Image.open("Tests/images/apng/delay.png") as im:
frames = []
durations = []
@@ -442,6 +486,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
)
with Image.open(test_file) as im:
+ assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 1
assert "duration" not in im.info
@@ -453,6 +498,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
@@ -463,12 +509,13 @@ 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
def test_apng_save_disposal(tmp_path: Path) -> None:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255))
@@ -569,7 +616,7 @@ def test_apng_save_disposal(tmp_path: Path) -> None:
def test_apng_save_disposal_previous(tmp_path: Path) -> None:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
size = (128, 64)
blue = Image.new("RGBA", size, (0, 0, 255, 255))
red = Image.new("RGBA", size, (255, 0, 0, 255))
@@ -591,7 +638,7 @@ def test_apng_save_disposal_previous(tmp_path: Path) -> None:
def test_apng_save_blend(tmp_path: Path) -> None:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255))
@@ -659,7 +706,7 @@ def test_apng_save_blend(tmp_path: Path) -> None:
def test_apng_save_size(tmp_path: Path) -> None:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im = Image.new("L", (100, 100))
im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
@@ -683,7 +730,7 @@ def test_seek_after_close() -> None:
def test_different_modes_in_later_frames(
mode: str, default_image: bool, duplicate: bool, tmp_path: Path
) -> None:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im = Image.new("L", (1, 1))
im.save(
@@ -697,7 +744,7 @@ def test_different_modes_in_later_frames(
def test_different_durations(tmp_path: Path) -> None:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
with Image.open("Tests/images/apng/different_durations.png") as im:
for _ in range(3):
diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py
new file mode 100644
index 000000000..bd87947c0
--- /dev/null
+++ b/Tests/test_file_avif.py
@@ -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(
+ [
+ '',
+ '',
+ ' ',
+ ' ',
+ " ",
+ "",
+ '',
+ ]
+ )
+ 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)
diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py
index 9f2de8f98..9f50df22d 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -46,7 +46,7 @@ def test_invalid_file() -> None:
def test_save(tmp_path: Path) -> None:
- f = str(tmp_path / "temp.blp")
+ f = tmp_path / "temp.blp"
for version in ("BLP1", "BLP2"):
im = hopper("P")
@@ -56,7 +56,7 @@ def test_save(tmp_path: Path) -> None:
assert_image_equal(im.convert("RGB"), reloaded)
with Image.open("Tests/images/transparent.png") as im:
- f = str(tmp_path / "temp.blp")
+ f = tmp_path / "temp.blp"
im.convert("P").save(f, blp_version=version)
with Image.open(f) as reloaded:
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index 2ff4160bd..746b2e180 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -15,25 +15,19 @@ from .helper import (
)
-def test_sanity(tmp_path: Path) -> None:
- def roundtrip(im: Image.Image) -> None:
- outfile = str(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:
@@ -66,7 +60,7 @@ def test_small_palette(tmp_path: Path) -> None:
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
im.putpalette(colors)
- out = str(tmp_path / "temp.bmp")
+ out = tmp_path / "temp.bmp"
im.save(out)
with Image.open(out) as reloaded:
@@ -74,7 +68,7 @@ def test_small_palette(tmp_path: Path) -> None:
def test_save_too_large(tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.bmp")
+ outfile = tmp_path / "temp.bmp"
with Image.new("RGB", (1, 1)) as im:
im._size = (37838, 37838)
with pytest.raises(ValueError):
@@ -96,7 +90,7 @@ def test_dpi() -> None:
def test_save_bmp_with_dpi(tmp_path: Path) -> None:
# Test for #1301
# Arrange
- outfile = str(tmp_path / "temp.jpg")
+ outfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["dpi"] == (95.98654816726399, 95.98654816726399)
@@ -112,7 +106,7 @@ def test_save_bmp_with_dpi(tmp_path: Path) -> None:
def test_save_float_dpi(tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.bmp")
+ outfile = tmp_path / "temp.bmp"
with Image.open("Tests/images/hopper.bmp") as im:
im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
with Image.open(outfile) as reloaded:
@@ -152,7 +146,7 @@ def test_dib_header_size(header_size: int, path: str) -> None:
def test_save_dib(tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.dib")
+ outfile = tmp_path / "temp.dib"
with Image.open("Tests/images/clipboard.dib") as im:
im.save(outfile)
@@ -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"
diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py
index fc8920317..362578c56 100644
--- a/Tests/test_file_bufrstub.py
+++ b/Tests/test_file_bufrstub.py
@@ -43,7 +43,7 @@ def test_load() -> None:
def test_save(tmp_path: Path) -> None:
# Arrange
im = hopper()
- tmpfile = str(tmp_path / "temp.bufr")
+ tmpfile = tmp_path / "temp.bufr"
# Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError):
@@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None:
im.load()
assert handler.is_loaded()
- temp_file = str(tmp_path / "temp.bufr")
+ temp_file = tmp_path / "temp.bufr"
im.save(temp_file)
assert handler.saved
diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py
index ab6b9f983..e9d88dd39 100644
--- a/Tests/test_file_dcx.py
+++ b/Tests/test_file_dcx.py
@@ -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
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index 7cc4d79d4..3388fce16 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -9,7 +9,13 @@ import pytest
from PIL import DdsImagePlugin, Image
-from .helper import assert_image_equal, assert_image_equal_tofile, hopper
+from .helper import (
+ assert_image_equal,
+ assert_image_equal_tofile,
+ assert_image_similar,
+ assert_image_similar_tofile,
+ hopper,
+)
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
@@ -109,6 +115,32 @@ def test_sanity_ati1_bc4u(image_path: str) -> None:
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
+def test_dx10_bc2(tmp_path: Path) -> None:
+ out = tmp_path / "temp.dds"
+ with Image.open(TEST_FILE_DXT3) as im:
+ im.save(out, pixel_format="BC2")
+
+ with Image.open(out) as reloaded:
+ assert reloaded.format == "DDS"
+ assert reloaded.mode == "RGBA"
+ assert reloaded.size == (256, 256)
+
+ assert_image_similar(im, reloaded, 3.81)
+
+
+def test_dx10_bc3(tmp_path: Path) -> None:
+ out = tmp_path / "temp.dds"
+ with Image.open(TEST_FILE_DXT5) as im:
+ im.save(out, pixel_format="BC3")
+
+ with Image.open(out) as reloaded:
+ assert reloaded.format == "DDS"
+ assert reloaded.mode == "RGBA"
+ assert reloaded.size == (256, 256)
+
+ assert_image_similar(im, reloaded, 3.69)
+
+
@pytest.mark.parametrize(
"image_path",
(
@@ -368,9 +400,9 @@ def test_not_implemented(test_file: str) -> None:
def test_save_unsupported_mode(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.dds")
+ out = tmp_path / "temp.dds"
im = hopper("HSV")
- with pytest.raises(OSError):
+ with pytest.raises(OSError, match="cannot write mode HSV as DDS"):
im.save(out)
@@ -384,10 +416,98 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
],
)
def test_save(mode: str, test_file: str, tmp_path: Path) -> None:
- out = str(tmp_path / "temp.dds")
+ out = tmp_path / "temp.dds"
with Image.open(test_file) as im:
assert im.mode == mode
im.save(out)
- with Image.open(out) as reloaded:
- assert_image_equal(im, reloaded)
+ assert_image_equal_tofile(im, out)
+
+
+def test_save_unsupported_pixel_format(tmp_path: Path) -> None:
+ out = tmp_path / "temp.dds"
+ im = hopper()
+ with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"):
+ im.save(out, pixel_format="UNKNOWN")
+
+
+def test_save_dxt1(tmp_path: Path) -> None:
+ # RGB
+ out = tmp_path / "temp.dds"
+ with Image.open(TEST_FILE_DXT1) as im:
+ im.convert("RGB").save(out, pixel_format="DXT1")
+ assert_image_similar_tofile(im, out, 1.84)
+
+ # RGBA
+ im_alpha = im.copy()
+ im_alpha.putpixel((0, 0), (0, 0, 0, 0))
+ im_alpha.save(out, pixel_format="DXT1")
+ with Image.open(out) as reloaded:
+ assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0)
+
+ # L
+ im_l = im.convert("L")
+ im_l.save(out, pixel_format="DXT1")
+ assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07)
+
+ # LA
+ im_alpha.convert("LA").save(out, pixel_format="DXT1")
+ with Image.open(out) as reloaded:
+ assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0)
+
+
+def test_save_dxt3(tmp_path: Path) -> None:
+ # RGB
+ out = tmp_path / "temp.dds"
+ with Image.open(TEST_FILE_DXT3) as im:
+ im_rgb = im.convert("RGB")
+ im_rgb.save(out, pixel_format="DXT3")
+ assert_image_similar_tofile(im_rgb.convert("RGBA"), out, 1.26)
+
+ # RGBA
+ im.save(out, pixel_format="DXT3")
+ assert_image_similar_tofile(im, out, 3.81)
+
+ # L
+ im_l = im.convert("L")
+ im_l.save(out, pixel_format="DXT3")
+ assert_image_similar_tofile(im_l.convert("RGBA"), out, 5.89)
+
+ # LA
+ im_la = im.convert("LA")
+ im_la.save(out, pixel_format="DXT3")
+ assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.44)
+
+
+def test_save_dxt5(tmp_path: Path) -> None:
+ # RGB
+ out = tmp_path / "temp.dds"
+ with Image.open(TEST_FILE_DXT1) as im:
+ im.convert("RGB").save(out, pixel_format="DXT5")
+ assert_image_similar_tofile(im, out, 1.84)
+
+ # RGBA
+ with Image.open(TEST_FILE_DXT5) as im_rgba:
+ im_rgba.save(out, pixel_format="DXT5")
+ assert_image_similar_tofile(im_rgba, out, 3.69)
+
+ # L
+ im_l = im.convert("L")
+ im_l.save(out, pixel_format="DXT5")
+ assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07)
+
+ # LA
+ im_la = im_rgba.convert("LA")
+ im_la.save(out, pixel_format="DXT5")
+ assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.32)
+
+
+def test_save_dx10_bc5(tmp_path: Path) -> None:
+ out = tmp_path / "temp.dds"
+ with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im:
+ im.save(out, pixel_format="BC5")
+ assert_image_similar_tofile(im, out, 9.56)
+
+ im = hopper("L")
+ with pytest.raises(OSError, match="only RGB mode can be written as BC5"):
+ im.save(out, pixel_format="BC5")
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index a0c2f9216..b484a8cfa 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -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"
@@ -239,7 +243,7 @@ def test_transparency() -> None:
def test_file_object(tmp_path: Path) -> None:
# issue 479
with Image.open(FILE1) as image1:
- with open(str(tmp_path / "temp.eps"), "wb") as fh:
+ with open(tmp_path / "temp.eps", "wb") as fh:
image1.save(fh, "EPS")
@@ -274,7 +278,7 @@ def test_1(filename: str) -> None:
def test_image_mode_not_supported(tmp_path: Path) -> None:
im = hopper("RGBA")
- tmpfile = str(tmp_path / "temp.eps")
+ tmpfile = tmp_path / "temp.eps"
with pytest.raises(ValueError):
im.save(tmpfile)
@@ -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")
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index 8adbd30f5..81df1ab0b 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import io
import warnings
import pytest
@@ -21,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)
@@ -28,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"
@@ -111,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
@@ -132,6 +140,15 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
+def test_missing_frame_size() -> None:
+ with open(animated_test_file, "rb") as fp:
+ data = fp.read()
+ data = data[:6188]
+ with Image.open(io.BytesIO(data)) as im:
+ with pytest.raises(EOFError, match="missing frame size"):
+ im.seek(1)
+
+
def test_seek_tell() -> None:
with Image.open(animated_test_file) as im:
layer_number = im.tell()
@@ -156,10 +173,14 @@ 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")
+ with pytest.raises(ValueError, match="cannot seek to frame 52"):
+ im._seek(52)
+
@pytest.mark.parametrize(
"test_file",
diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py
index e32f30a01..8d8064692 100644
--- a/Tests/test_file_fpx.py
+++ b/Tests/test_file_fpx.py
@@ -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
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 2254178d5..20d58a9dd 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -228,7 +228,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None:
def test_full_palette_second_frame(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("P", (1, 256))
full_palette_im = Image.new("P", (1, 256))
@@ -249,7 +249,7 @@ def test_full_palette_second_frame(tmp_path: Path) -> None:
def test_roundtrip(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = hopper()
im.save(out)
with Image.open(out) as reread:
@@ -258,7 +258,7 @@ def test_roundtrip(tmp_path: Path) -> None:
def test_roundtrip2(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/403
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
with Image.open(TEST_GIF) as im:
im2 = im.copy()
im2.save(out)
@@ -268,7 +268,7 @@ def test_roundtrip2(tmp_path: Path) -> None:
def test_roundtrip_save_all(tmp_path: Path) -> None:
# Single frame image
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = hopper()
im.save(out, save_all=True)
with Image.open(out) as reread:
@@ -276,7 +276,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
# Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im.save(out, save_all=True)
with Image.open(out) as reread:
@@ -284,7 +284,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
def test_roundtrip_save_all_1(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1)
im.save(out, save_all=True, append_images=[im2])
@@ -329,7 +329,7 @@ def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im:
info = im.info.copy()
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im.save(out, save_all=True)
with Image.open(out) as reread:
for header in important_headers:
@@ -345,7 +345,7 @@ def test_palette_handling(tmp_path: Path) -> None:
im = im.resize((100, 100), Image.Resampling.LANCZOS)
im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
- f = str(tmp_path / "temp.gif")
+ f = tmp_path / "temp.gif"
im2.save(f, optimize=True)
with Image.open(f) as reloaded:
@@ -356,7 +356,7 @@ def test_palette_434(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/434
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out)
@@ -402,6 +402,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:
@@ -410,6 +411,10 @@ def test_seek() -> None:
except EOFError:
assert frame_count == 5
+ img.seek(0)
+ with pytest.raises(ValueError, match="cannot seek to frame 2"):
+ img._seek(2)
+
def test_seek_info() -> None:
with Image.open("Tests/images/iss634.gif") as im:
@@ -442,10 +447,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)
@@ -455,6 +462,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)
@@ -462,17 +470,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
@@ -491,6 +502,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)
@@ -514,6 +526,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)
@@ -567,6 +580,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)
@@ -595,15 +609,16 @@ def test_previous_frame_loaded() -> None:
def test_save_dispose(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#111"),
Image.new("L", (100, 100), "#222"),
]
- for method in range(0, 4):
+ for method in range(4):
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
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
@@ -617,13 +632,14 @@ 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
def test_dispose2_palette(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
# Four colors: white, gray, black, red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
@@ -657,7 +673,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
def test_dispose2_diff(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
# 4 frames: red/blue, red/red, blue/blue, red/blue
circles = [
@@ -699,7 +715,7 @@ def test_dispose2_diff(tmp_path: Path) -> None:
def test_dispose2_background(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im_list = []
@@ -725,7 +741,7 @@ def test_dispose2_background(tmp_path: Path) -> None:
def test_dispose2_background_frame(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im_list = [Image.new("RGBA", (1, 20))]
@@ -739,11 +755,12 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
with Image.open(out) as im:
+ assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == 3
def test_dispose2_previous_frame(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("P", (100, 100))
im.info["transparency"] = 0
@@ -762,7 +779,7 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None:
def test_dispose2_without_transparency(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("P", (100, 100))
@@ -777,7 +794,7 @@ def test_dispose2_without_transparency(tmp_path: Path) -> None:
def test_transparency_in_second_frame(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0
@@ -807,7 +824,7 @@ def test_no_transparency_in_second_frame() -> None:
def test_remapped_transparency(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("P", (1, 2))
im2 = im.copy()
@@ -825,7 +842,7 @@ def test_remapped_transparency(tmp_path: Path) -> None:
def test_duration(tmp_path: Path) -> None:
duration = 1000
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
# Check that the argument has priority over the info settings
@@ -839,7 +856,7 @@ def test_duration(tmp_path: Path) -> None:
def test_multiple_duration(tmp_path: Path) -> None:
duration_list = [1000, 2000, 3000]
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#111"),
@@ -874,7 +891,7 @@ def test_multiple_duration(tmp_path: Path) -> None:
def test_roundtrip_info_duration(tmp_path: Path) -> None:
duration_list = [100, 500, 500]
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
with Image.open("Tests/images/transparent_dispose.gif") as im:
assert [
frame.info["duration"] for frame in ImageSequence.Iterator(im)
@@ -889,7 +906,7 @@ def test_roundtrip_info_duration(tmp_path: Path) -> None:
def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
with Image.open("Tests/images/duplicate_frame.gif") as im:
assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
1000,
@@ -907,7 +924,7 @@ def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
def test_identical_frames(tmp_path: Path) -> None:
duration_list = [1000, 1500, 2000, 4000]
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
@@ -920,6 +937,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
@@ -940,7 +959,7 @@ def test_identical_frames(tmp_path: Path) -> None:
def test_identical_frames_to_single_frame(
duration: int | list[int], tmp_path: Path
) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
@@ -949,6 +968,8 @@ def test_identical_frames_to_single_frame(
im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
with Image.open(out) as reread:
+ assert isinstance(reread, GifImagePlugin.GifImageFile)
+
# Assert that all frames were combined
assert reread.n_frames == 1
@@ -957,7 +978,7 @@ def test_identical_frames_to_single_frame(
def test_loop_none(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
im.save(out, loop=None)
with Image.open(out) as reread:
@@ -967,7 +988,7 @@ def test_loop_none(tmp_path: Path) -> None:
def test_number_of_loops(tmp_path: Path) -> None:
number_of_loops = 2
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
im.save(out, loop=number_of_loops)
with Image.open(out) as reread:
@@ -983,7 +1004,7 @@ def test_number_of_loops(tmp_path: Path) -> None:
def test_background(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
im.info["background"] = 1
im.save(out)
@@ -992,7 +1013,7 @@ def test_background(tmp_path: Path) -> None:
def test_webp_background(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
# Test opaque WebP background
if features.check("webp"):
@@ -1010,7 +1031,7 @@ def test_comment(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0"
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
im.info["comment"] = b"Test comment text"
im.save(out)
@@ -1027,7 +1048,7 @@ def test_comment(tmp_path: Path) -> None:
def test_comment_over_255(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("L", (100, 100), "#000")
comment = b"Test comment text"
while len(comment) < 256:
@@ -1053,7 +1074,7 @@ def test_read_multiple_comment_blocks() -> None:
def test_empty_string_comment(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
with Image.open("Tests/images/chi.gif") as im:
assert "comment" in im.info
@@ -1087,7 +1108,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
assert "comment" not in im.info
# Test that a saved image keeps the comment
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
with Image.open("Tests/images/dispose_prev.gif") as im:
im.save(out, save_all=True, comment="Test")
@@ -1097,7 +1118,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
def test_version(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
def assert_version_after_save(im: Image.Image, version: bytes) -> None:
im.save(out)
@@ -1127,7 +1148,7 @@ def test_version(tmp_path: Path) -> None:
def test_append_images(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
# Test appending single frame images
im = Image.new("RGB", (100, 100), "#f00")
@@ -1135,6 +1156,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
@@ -1144,6 +1173,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
@@ -1152,11 +1182,12 @@ 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
def test_append_different_size_image(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("RGB", (100, 100))
bigger_im = Image.new("RGB", (200, 200), "#f00")
@@ -1183,7 +1214,7 @@ def test_transparent_optimize(tmp_path: Path) -> None:
im.frombytes(data)
im.putpalette(palette)
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im.save(out, transparency=im.getpixel((252, 0)))
with Image.open(out) as reloaded:
@@ -1191,7 +1222,7 @@ def test_transparent_optimize(tmp_path: Path) -> None:
def test_removed_transparency(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("RGB", (256, 1))
for x in range(256):
@@ -1206,7 +1237,7 @@ def test_removed_transparency(tmp_path: Path) -> None:
def test_rgb_transparency(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
# Single frame
im = Image.new("RGB", (1, 1))
@@ -1228,7 +1259,7 @@ def test_rgb_transparency(tmp_path: Path) -> None:
def test_rgba_transparency(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = hopper("P")
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
@@ -1238,25 +1269,26 @@ def test_rgba_transparency(tmp_path: Path) -> None:
assert_image_equal(hopper("P").convert("RGB"), reloaded)
-def test_background_outside_palettte(tmp_path: Path) -> None:
+def test_background_outside_palettte() -> None:
with Image.open("Tests/images/background_outside_palette.gif") as im:
im.seek(1)
assert im.info["background"] == 255
def test_bbox(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("RGB", (100, 100), "#fff")
ims = [Image.new("RGB", (100, 100), "#000")]
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
def test_bbox_alpha(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im = Image.new("RGBA", (1, 2), (255, 0, 0, 255))
im.putpixel((0, 1), (255, 0, 0, 0))
@@ -1264,6 +1296,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
@@ -1275,7 +1308,7 @@ def test_palette_save_L(tmp_path: Path) -> None:
palette = im.getpalette()
assert palette is not None
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im_l.save(out, palette=bytes(palette))
with Image.open(out) as reloaded:
@@ -1286,7 +1319,7 @@ def test_palette_save_P(tmp_path: Path) -> None:
im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1)
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
with Image.open(out) as reloaded:
@@ -1302,7 +1335,7 @@ def test_palette_save_duplicate_entries(tmp_path: Path) -> None:
im.putpalette((0, 0, 0, 0, 0, 0))
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1])
with Image.open(out) as reloaded:
@@ -1317,7 +1350,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
frame.putpalette(color)
frames.append(frame)
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
frames[0].save(
out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]
)
@@ -1340,7 +1373,7 @@ def test_palette_save_ImagePalette(tmp_path: Path) -> None:
im = hopper("P")
palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3)
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im.save(out, palette=palette)
with Image.open(out) as reloaded:
@@ -1353,7 +1386,7 @@ def test_save_I(tmp_path: Path) -> None:
im = hopper("I")
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im.save(out)
with Image.open(out) as reloaded:
@@ -1415,6 +1448,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
@@ -1437,7 +1471,7 @@ def test_missing_background() -> None:
def test_saving_rgba(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
with Image.open("Tests/images/transparent.png") as im:
im.save(out)
@@ -1448,7 +1482,7 @@ def test_saving_rgba(tmp_path: Path) -> None:
@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False}))
def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
- out = str(tmp_path / "temp.gif")
+ out = tmp_path / "temp.gif"
im1 = Image.new("P", (100, 100))
d = ImageDraw.Draw(im1)
@@ -1462,4 +1496,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
im1.save(out, save_all=True, append_images=[im2], **params)
with Image.open(out) as reloaded:
+ assert isinstance(reloaded, GifImagePlugin.GifImageFile)
assert reloaded.n_frames == 2
diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py
index e8d5f1705..08862113b 100644
--- a/Tests/test_file_gimppalette.py
+++ b/Tests/test_file_gimppalette.py
@@ -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,3 +37,36 @@ def test_get_palette() -> None:
# Assert
assert mode == "RGB"
+ assert len(palette) / 3 == size
+
+
+def test_frombytes() -> None:
+ # Test that __init__ stops reading after 260 lines
+ with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
+ custom_data = fp.read()
+ custom_data += b"#\n" * 300 + b" 0 0 0 Index 12"
+ b = BytesIO(custom_data)
+ palette = GimpPaletteFile(b)
+ assert len(palette.palette) / 3 == 8
+
+ # Test that __init__ only reads 256 entries
+ with open("Tests/images/full_gimp_palette.gpl", "rb") as fp:
+ full_data = fp.read()
+ data = full_data.replace(b"#\n", b"") + b" 0 0 0 Index 256"
+ b = BytesIO(data)
+ palette = GimpPaletteFile(b)
+ assert len(palette.palette) / 3 == 256
+
+ # Test that frombytes() can read beyond that
+ palette = GimpPaletteFile.frombytes(data)
+ assert len(palette.palette) / 3 == 257
+
+ # Test that __init__ raises an error if a comment is too long
+ data = full_data[:-1] + b"a" * 100
+ b = BytesIO(data)
+ with pytest.raises(SyntaxError, match="bad palette file"):
+ palette = GimpPaletteFile(b)
+
+ # Test that frombytes() can read the data regardless
+ palette = GimpPaletteFile.frombytes(data)
+ assert len(palette.palette) / 3 == 256
diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py
index 02e464ff1..960e5f4be 100644
--- a/Tests/test_file_gribstub.py
+++ b/Tests/test_file_gribstub.py
@@ -43,7 +43,7 @@ def test_load() -> None:
def test_save(tmp_path: Path) -> None:
# Arrange
im = hopper()
- tmpfile = str(tmp_path / "temp.grib")
+ tmpfile = tmp_path / "temp.grib"
# Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError):
@@ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None:
im.load()
assert handler.is_loaded()
- temp_file = str(tmp_path / "temp.grib")
+ temp_file = tmp_path / "temp.grib"
im.save(temp_file)
assert handler.saved
diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py
index 024be9e80..e4f09a09c 100644
--- a/Tests/test_file_hdf5stub.py
+++ b/Tests/test_file_hdf5stub.py
@@ -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):
@@ -81,7 +81,7 @@ def test_handler(tmp_path: Path) -> None:
im.load()
assert handler.is_loaded()
- temp_file = str(tmp_path / "temp.h5")
+ temp_file = tmp_path / "temp.h5"
im.save(temp_file)
assert handler.saved
diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py
index 94f16aeec..2dabfd2f3 100644
--- a/Tests/test_file_icns.py
+++ b/Tests/test_file_icns.py
@@ -43,7 +43,7 @@ def test_load() -> None:
def test_save(tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.icns")
+ temp_file = tmp_path / "temp.icns"
with Image.open(TEST_FILE) as im:
im.save(temp_file)
@@ -60,7 +60,7 @@ def test_save(tmp_path: Path) -> None:
def test_save_append_images(tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.icns")
+ temp_file = tmp_path / "temp.icns"
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128))
with Image.open(TEST_FILE) as im:
@@ -69,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None:
assert_image_similar_tofile(im, temp_file, 1)
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"
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index 2f5e4ca5a..5d2ace35e 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -41,7 +41,7 @@ def test_black_and_white() -> None:
def test_palette(tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.ico")
+ temp_file = tmp_path / "temp.ico"
im = Image.new("P", (16, 16))
im.save(temp_file)
@@ -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
@@ -88,12 +89,13 @@ def test_save_to_bytes() -> None:
def test_getpixel(tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.ico")
+ temp_file = tmp_path / "temp.ico"
im = hopper()
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)
@@ -101,8 +103,8 @@ def test_getpixel(tmp_path: Path) -> None:
def test_no_duplicates(tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.ico")
- temp_file2 = str(tmp_path / "temp2.ico")
+ temp_file = tmp_path / "temp.ico"
+ temp_file2 = tmp_path / "temp2.ico"
im = hopper()
sizes = [(32, 32), (64, 64)]
@@ -115,8 +117,8 @@ def test_no_duplicates(tmp_path: Path) -> None:
def test_different_bit_depths(tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.ico")
- temp_file2 = str(tmp_path / "temp2.ico")
+ temp_file = tmp_path / "temp.ico"
+ temp_file2 = tmp_path / "temp2.ico"
im = hopper()
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
@@ -132,8 +134,8 @@ def test_different_bit_depths(tmp_path: Path) -> None:
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
# Test that only matching sizes of different bit depths are saved
- temp_file3 = str(tmp_path / "temp3.ico")
- temp_file4 = str(tmp_path / "temp4.ico")
+ temp_file3 = tmp_path / "temp3.ico"
+ temp_file4 = tmp_path / "temp4.ico"
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
im.save(
@@ -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)
@@ -186,7 +190,7 @@ def test_save_256x256(tmp_path: Path) -> None:
"""Issue #2264 https://github.com/python-pillow/Pillow/issues/2264"""
# Arrange
with Image.open("Tests/images/hopper_256x256.ico") as im:
- outfile = str(tmp_path / "temp_saved_hopper_256x256.ico")
+ outfile = tmp_path / "temp_saved_hopper_256x256.ico"
# Act
im.save(outfile)
@@ -202,7 +206,7 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None:
"""
# Arrange
with Image.open("Tests/images/python.ico") as im: # 16x16, 32x32, 48x48
- outfile = str(tmp_path / "temp_saved_python.ico")
+ outfile = tmp_path / "temp_saved_python.ico"
# Act
im.save(outfile)
@@ -215,10 +219,11 @@ def test_save_append_images(tmp_path: Path) -> None:
# append_images should be used for scaled down versions of the image
im = hopper("RGBA")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0))
- outfile = str(tmp_path / "temp_saved_multi_icon.ico")
+ outfile = tmp_path / "temp_saved_multi_icon.ico"
im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im])
with Image.open(outfile) as reread:
+ assert isinstance(reread, IcoImagePlugin.IcoImageFile)
assert_image_equal(reread, hopper("RGBA"))
reread.size = (32, 32)
@@ -235,7 +240,7 @@ def test_unexpected_size() -> None:
def test_draw_reloaded(tmp_path: Path) -> None:
with Image.open(TEST_ICO_FILE) as im:
- outfile = str(tmp_path / "temp_saved_hopper_draw.ico")
+ outfile = tmp_path / "temp_saved_hopper_draw.ico"
draw = ImageDraw.Draw(im)
draw.line((0, 0) + im.size, "#f00")
diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py
index d29998801..55c6b7305 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -23,7 +23,7 @@ def test_sanity() -> None:
def test_name_limit(tmp_path: Path) -> None:
- out = str(tmp_path / ("name_limit_test" * 7 + ".im"))
+ out = tmp_path / ("name_limit_test" * 7 + ".im")
with Image.open(TEST_IM) as im:
im.save(out)
assert filecmp.cmp(out, "Tests/images/hopper_long_name.im")
@@ -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
@@ -87,7 +89,7 @@ def test_eoferror() -> None:
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
def test_roundtrip(mode: str, tmp_path: Path) -> None:
- out = str(tmp_path / "temp.im")
+ out = tmp_path / "temp.im"
im = hopper(mode)
im.save(out)
assert_image_equal_tofile(im, out)
@@ -98,7 +100,7 @@ def test_small_palette(tmp_path: Path) -> None:
colors = [0, 1, 2]
im.putpalette(colors)
- out = str(tmp_path / "temp.im")
+ out = tmp_path / "temp.im"
im.save(out)
with Image.open(out) as reloaded:
@@ -106,7 +108,7 @@ def test_small_palette(tmp_path: Path) -> None:
def test_save_unsupported_mode(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.im")
+ out = tmp_path / "temp.im"
im = hopper("HSV")
with pytest.raises(ValueError):
im.save(out)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index a2481c336..79f0ec1a8 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -83,7 +83,7 @@ class TestFileJpeg:
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None:
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
im = Image.new("RGB", size)
with pytest.raises(ValueError):
im.save(f)
@@ -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",
@@ -194,7 +195,7 @@ class TestFileJpeg:
icc_profile = im1.info["icc_profile"]
assert len(icc_profile) == 3144
# Roundtrip via physical file.
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
im1.save(f, icc_profile=icc_profile)
with Image.open(f) as im2:
assert im2.info.get("icc_profile") == icc_profile
@@ -238,7 +239,7 @@ class TestFileJpeg:
# Sometimes the meta data on the icc_profile block is bigger than
# Image.MAXBLOCK or the image size.
with Image.open("Tests/images/icc_profile_big.jpg") as im:
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
icc_profile = im.info["icc_profile"]
# Should not raise OSError for image with icc larger than image size.
im.save(
@@ -250,11 +251,11 @@ class TestFileJpeg:
)
with Image.open("Tests/images/flower2.jpg") as im:
- f = str(tmp_path / "temp2.jpg")
+ f = tmp_path / "temp2.jpg"
im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955)
with Image.open("Tests/images/flower2.jpg") as im:
- f = str(tmp_path / "temp3.jpg")
+ f = tmp_path / "temp3.jpg"
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
def test_optimize(self) -> None:
@@ -268,7 +269,7 @@ class TestFileJpeg:
def test_optimize_large_buffer(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
# this requires ~ 1.5x Image.MAXBLOCK
im = Image.new("RGB", (4096, 4096), 0xFF3333)
im.save(f, format="JPEG", optimize=True)
@@ -288,13 +289,13 @@ class TestFileJpeg:
assert im1_bytes >= im3_bytes
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
# this requires ~ 1.5x Image.MAXBLOCK
im = Image.new("RGB", (4096, 4096), 0xFF3333)
im.save(f, format="JPEG", progressive=True)
def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None:
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
im = self.gen_random_image((255, 255))
# this requires more bytes than pixels in the image
im.save(f, format="JPEG", progressive=True, quality=100)
@@ -307,7 +308,7 @@ class TestFileJpeg:
def test_large_exif(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
im = hopper()
im.save(f, "JPEG", quality=90, exif=b"1" * 65533)
@@ -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()
@@ -335,7 +338,7 @@ class TestFileJpeg:
assert exif[gps_index] == expected_exif_gps
# Writing
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
exif = Image.Exif()
exif[gps_index] = expected_exif_gps
hopper().save(f, exif=exif)
@@ -500,20 +503,21 @@ 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:
# RGB
with Image.open("Tests/images/hopper.jpg") as im:
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
im.save(f, quality="keep")
# Grayscale
with Image.open("Tests/images/hopper_gray.jpg") as im:
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
im.save(f, quality="keep")
# CMYK
with Image.open("Tests/images/pil_sample_cmyk.jpg") as im:
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
im.save(f, quality="keep")
def test_junk_jpeg_header(self) -> 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)
@@ -726,7 +734,7 @@ class TestFileJpeg:
def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None:
im = self.gen_random_image((512, 512))
- f = str(tmp_path / "temp.jpeg")
+ f = tmp_path / "temp.jpeg"
im.save(f, quality=100, optimize=True)
with Image.open(f) as reloaded:
@@ -762,7 +770,7 @@ class TestFileJpeg:
def test_save_tiff_with_dpi(self, tmp_path: Path) -> None:
# Arrange
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/hopper.tif") as im:
# Act
im.save(outfile, "JPEG", dpi=im.info["dpi"])
@@ -773,7 +781,7 @@ class TestFileJpeg:
assert im.info["dpi"] == reloaded.info["dpi"]
def test_save_dpi_rounding(self, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.jpg")
+ outfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.jpg") as im:
im.save(outfile, dpi=(72.2, 72.2))
@@ -859,7 +867,7 @@ class TestFileJpeg:
exif = im.getexif()
assert exif[282] == 180
- out = str(tmp_path / "out.jpg")
+ out = tmp_path / "out.jpg"
with warnings.catch_warnings():
warnings.simplefilter("error")
@@ -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"]
@@ -1005,7 +1014,7 @@ class TestFileJpeg:
assert im.getxmp() == {"xmpmeta": None}
def test_save_xmp(self, tmp_path: Path) -> None:
- f = str(tmp_path / "temp.jpg")
+ f = tmp_path / "temp.jpg"
im = hopper()
im.save(f, xmp=b"XMP test")
with Image.open(f) as reloaded:
@@ -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):
@@ -1094,7 +1104,7 @@ class TestFileJpeg:
@skip_unless_feature("jpg")
class TestFileCloseW32:
def test_fd_leak(self, tmp_path: Path) -> None:
- tmpfile = str(tmp_path / "temp.jpg")
+ tmpfile = tmp_path / "temp.jpg"
with Image.open("Tests/images/hopper.jpg") as im:
im.save(tmpfile)
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index 5748fa5a1..a5365a90d 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -99,7 +99,7 @@ def test_bytesio(card: ImageFile.ImageFile) -> None:
def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
im.load()
- outfile = str(tmp_path / "temp_test-card.png")
+ outfile = tmp_path / "temp_test-card.png"
im.save(outfile)
assert_image_similar(im, card, 1.0e-3)
@@ -213,7 +213,7 @@ def test_header_errors() -> None:
def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp_layers.jp2")
+ outfile = tmp_path / "temp_layers.jp2"
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
card.save(outfile, quality_layers=quality_layers)
@@ -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)
@@ -289,7 +291,7 @@ def test_mct(card: ImageFile.ImageFile) -> None:
def test_sgnd(tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.jp2")
+ outfile = tmp_path / "temp.jp2"
im = Image.new("L", (1, 1))
im.save(outfile)
@@ -313,6 +315,18 @@ def test_rgba(ext: str) -> None:
assert im.mode == "RGBA"
+def test_grayscale_four_channels() -> None:
+ with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp:
+ data = fp.read()
+
+ # Change color space to OPJ_CLRSPC_GRAY
+ data = data[:76] + b"\x11" + data[77:]
+
+ with Image.open(BytesIO(data)) as im:
+ im.load()
+ assert im.mode == "RGBA"
+
+
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
@@ -443,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:
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 369c2db1b..1ec39eba5 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -36,10 +36,11 @@ 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.
- out = str(tmp_path / "temp.png")
+ out = tmp_path / "temp.png"
im.save(out)
out_bytes = io.BytesIO()
@@ -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)
@@ -123,7 +124,7 @@ class TestFileLibTiff(LibTiffTestCase):
"""Checking to see that the saved image is the same as what we wrote"""
test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig:
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
rot = orig.transpose(Image.Transpose.ROTATE_90)
assert rot.size == (500, 500)
rot.save(out)
@@ -151,8 +152,9 @@ class TestFileLibTiff(LibTiffTestCase):
@pytest.mark.parametrize("legacy_api", (False, True))
def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None:
"""Test metadata writing through libtiff"""
- f = str(tmp_path / "temp.tiff")
+ f = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper_g4.tif") as img:
+ 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]
@@ -247,7 +251,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Extra samples really doesn't make sense in this application.
del new_ifd[338]
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, tiffinfo=new_ifd)
@@ -313,10 +317,11 @@ class TestFileLibTiff(LibTiffTestCase):
) -> None:
im = hopper()
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
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 (
@@ -347,14 +352,16 @@ class TestFileLibTiff(LibTiffTestCase):
)
def test_osubfiletype(self, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/g4_orientation_6.tif") as im:
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[OSUBFILETYPE] = 1
im.save(outfile)
def test_subifd(self, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/g4_orientation_6.tif") as im:
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[SUBIFD] = 10000
# Should not segfault
@@ -365,17 +372,18 @@ class TestFileLibTiff(LibTiffTestCase):
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
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"
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
# issue #1765
im = hopper("RGB")
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, dpi=(72, 72))
with Image.open(out) as reloaded:
@@ -383,7 +391,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g3_compression(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper_g4_500.tif") as i:
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
i.save(out, compression="group3")
with Image.open(out) as reread:
@@ -400,7 +408,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert b[0] == ord(b"\xe0")
assert b[1] == ord(b"\x01")
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
# out = "temp.le.tif"
im.save(out)
with Image.open(out) as reread:
@@ -420,7 +428,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert b[0] == ord(b"\x01")
assert b[1] == ord(b"\xe0")
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
im.save(out)
with Image.open(out) as reread:
assert reread.info["compression"] == im.info["compression"]
@@ -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:
- out = str(tmp_path / "temp.tif")
+ 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]
@@ -457,7 +468,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_blur(self, tmp_path: Path) -> None:
# test case from irc, how to do blur on b/w image
# and save to compressed tif.
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
with Image.open("Tests/images/pport_g4.tif") as im:
im = im.convert("L")
@@ -470,7 +481,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Test various tiff compressions and assert similar image content but reduced
# file sizes.
im = hopper("RGB")
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
im.save(out)
size_raw = os.path.getsize(out)
@@ -494,7 +505,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiff_jpeg_compression(self, tmp_path: Path) -> None:
im = hopper("RGB")
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
im.save(out, compression="tiff_jpeg")
with Image.open(out) as reloaded:
@@ -502,7 +513,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiff_deflate_compression(self, tmp_path: Path) -> None:
im = hopper("RGB")
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
im.save(out, compression="tiff_deflate")
with Image.open(out) as reloaded:
@@ -510,7 +521,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_quality(self, tmp_path: Path) -> None:
im = hopper("RGB")
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
with pytest.raises(ValueError):
im.save(out, compression="tiff_lzw", quality=50)
@@ -525,7 +536,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_cmyk_save(self, tmp_path: Path) -> None:
im = hopper("CMYK")
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
im.save(out, compression="tiff_adobe_deflate")
assert_image_equal_tofile(im, out)
@@ -534,19 +545,20 @@ class TestFileLibTiff(LibTiffTestCase):
def test_palette_save(
self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out)
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"))
def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB")
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
with pytest.raises(OSError):
im.save(out, compression=compression)
@@ -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
@@ -686,25 +701,29 @@ class TestFileLibTiff(LibTiffTestCase):
def test_save_ycbcr(self, tmp_path: Path) -> None:
im = hopper("YCbCr")
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
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
@@ -713,7 +732,7 @@ class TestFileLibTiff(LibTiffTestCase):
) -> None:
# issue 1597
with Image.open("Tests/images/rdf.tif") as im:
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
# this shouldn't crash
@@ -724,7 +743,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Test TIFF with tag 297 (Page Number) having value of 0 0.
# The first number is the current page number.
# The second is the total number of pages, zero means not available.
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
# Created by printing a page in Chrome to PDF, then:
# /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif
# -dNOPAUSE /tmp/test.pdf -c quit
@@ -736,7 +755,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_fd_duplication(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/1651
- tmpfile = str(tmp_path / "temp.tif")
+ tmpfile = tmp_path / "temp.tif"
with open(tmpfile, "wb") as f:
with open("Tests/images/g4-multi.tiff", "rb") as src:
f.write(src.read())
@@ -779,13 +798,14 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_profile = img.info["icc_profile"]
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
img.save(out, icc_profile=icc_profile)
with Image.open(out) as reloaded:
assert icc_profile == reloaded.info["icc_profile"]
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)
@@ -802,7 +822,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None:
# Arrange
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
# Created with ImageMagick: convert hopper.jpg hopper_jpg.tif
# Contains JPEGTables (347) tag
@@ -864,7 +884,7 @@ class TestFileLibTiff(LibTiffTestCase):
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
im = Image.new("F", (1, 1))
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out)
@@ -1008,7 +1028,7 @@ class TestFileLibTiff(LibTiffTestCase):
@pytest.mark.parametrize("compression", (None, "jpeg"))
def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None:
im = hopper()
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
tags = {
TiffImagePlugin.TILEWIDTH: 256,
@@ -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()
@@ -1140,16 +1172,14 @@ class TestFileLibTiff(LibTiffTestCase):
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
- with pytest.raises(OSError) as e:
- im.load()
-
# Assert that the error code is IMAGING_CODEC_MEMORY
- assert str(e.value) == "decoder error -9"
+ with pytest.raises(OSError, match="decoder error -9"):
+ im.load()
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256))
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
im.save(out, compression=compression)
with Image.open(out) as im:
@@ -1162,7 +1192,7 @@ class TestFileLibTiff(LibTiffTestCase):
self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
im = hopper("RGB").resize((256, 256))
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
if not argument:
monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18)
@@ -1178,13 +1208,13 @@ class TestFileLibTiff(LibTiffTestCase):
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
def test_save_zero(self, compression: str | None, tmp_path: Path) -> None:
im = Image.new("RGB", (0, 0))
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
with pytest.raises(SystemError):
im.save(out, compression=compression)
def test_save_many_compressed(self, tmp_path: Path) -> None:
im = hopper()
- out = str(tmp_path / "temp.tif")
+ out = tmp_path / "temp.tif"
for _ in range(10000):
im.save(out, compression="jpeg")
diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py
index 617e1e89c..65ba80c20 100644
--- a/Tests/test_file_libtiff_small.py
+++ b/Tests/test_file_libtiff_small.py
@@ -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)
diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py
index 9a6f13ea3..9aeb306e4 100644
--- a/Tests/test_file_mic.py
+++ b/Tests/test_file_mic.py
@@ -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
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index 6b4f6423b..73838ef44 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -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)
diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py
index b0964aabe..8c91922bd 100644
--- a/Tests/test_file_msp.py
+++ b/Tests/test_file_msp.py
@@ -15,7 +15,7 @@ YA_EXTRA_DIR = "Tests/images/msp"
def test_sanity(tmp_path: Path) -> None:
- test_file = str(tmp_path / "temp.msp")
+ test_file = tmp_path / "temp.msp"
hopper("1").save(test_file)
@@ -84,7 +84,7 @@ def test_msp_v2() -> None:
def test_cannot_save_wrong_mode(tmp_path: Path) -> None:
# Arrange
im = hopper()
- filename = str(tmp_path / "temp.msp")
+ filename = tmp_path / "temp.msp"
# Act/Assert
with pytest.raises(OSError):
diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py
index 194f39b30..58208ba99 100644
--- a/Tests/test_file_palm.py
+++ b/Tests/test_file_palm.py
@@ -14,7 +14,7 @@ from .helper import assert_image_equal, hopper, magick_command
def helper_save_as_palm(tmp_path: Path, mode: str) -> None:
# Arrange
im = hopper(mode)
- outfile = str(tmp_path / ("temp_" + mode + ".palm"))
+ outfile = tmp_path / ("temp_" + mode + ".palm")
# Act
im.save(outfile)
@@ -25,7 +25,7 @@ def helper_save_as_palm(tmp_path: Path, mode: str) -> None:
def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image:
- outfile = str(tmp_path / "temp.png")
+ outfile = tmp_path / "temp.png"
rc = subprocess.call(
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
)
@@ -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"
diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py
index b3f38c3e5..5d7fd1c1b 100644
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import io
from pathlib import Path
import pytest
@@ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper
def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
- f = str(tmp_path / "temp.pcx")
+ f = tmp_path / "temp.pcx"
im.save(f)
with Image.open(f) as im2:
assert im2.mode == im.mode
@@ -30,12 +31,34 @@ def test_sanity(tmp_path: Path) -> None:
_roundtrip(tmp_path, im)
# Test an unsupported mode
- f = str(tmp_path / "temp.pcx")
+ f = tmp_path / "temp.pcx"
im = hopper("RGBA")
with pytest.raises(ValueError):
im.save(f)
+def test_bad_image_size() -> None:
+ with open("Tests/images/pil184.pcx", "rb") as fp:
+ data = fp.read()
+ data = data[:4] + b"\xff\xff" + data[6:]
+
+ b = io.BytesIO(data)
+ with pytest.raises(SyntaxError, match="bad PCX image size"):
+ with PcxImagePlugin.PcxImageFile(b):
+ pass
+
+
+def test_unknown_mode() -> None:
+ with open("Tests/images/pil184.pcx", "rb") as fp:
+ data = fp.read()
+ data = data[:3] + b"\xff" + data[4:]
+
+ b = io.BytesIO(data)
+ with pytest.raises(OSError, match="unknown PCX mode"):
+ with Image.open(b):
+ pass
+
+
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index 815686a52..bde1e3ab8 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -55,7 +55,7 @@ def test_save_alpha(tmp_path: Path, mode: str) -> None:
def test_p_alpha(tmp_path: Path) -> None:
# Arrange
- outfile = str(tmp_path / "temp.pdf")
+ outfile = tmp_path / "temp.pdf"
with Image.open("Tests/images/pil123p.png") as im:
assert im.mode == "P"
assert isinstance(im.info["transparency"], bytes)
@@ -80,7 +80,7 @@ def test_monochrome(tmp_path: Path) -> None:
def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("PA")
- outfile = str(tmp_path / "temp_PA.pdf")
+ outfile = tmp_path / "temp_PA.pdf"
with pytest.raises(ValueError):
im.save(outfile)
@@ -89,7 +89,7 @@ def test_unsupported_mode(tmp_path: Path) -> None:
def test_resolution(tmp_path: Path) -> None:
im = hopper()
- outfile = str(tmp_path / "temp.pdf")
+ outfile = tmp_path / "temp.pdf"
im.save(outfile, resolution=150)
with open(outfile, "rb") as fp:
@@ -117,7 +117,7 @@ def test_resolution(tmp_path: Path) -> None:
def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()
- outfile = str(tmp_path / "temp.pdf")
+ outfile = tmp_path / "temp.pdf"
im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp:
@@ -144,7 +144,7 @@ def test_save_all(tmp_path: Path) -> None:
# Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
- outfile = str(tmp_path / "temp.pdf")
+ outfile = tmp_path / "temp.pdf"
im.save(outfile, save_all=True)
assert os.path.isfile(outfile)
@@ -177,7 +177,7 @@ def test_save_all(tmp_path: Path) -> None:
def test_multiframe_normal_save(tmp_path: Path) -> None:
# Test saving a multiframe image without save_all
with Image.open("Tests/images/dispose_bgnd.gif") as im:
- outfile = str(tmp_path / "temp.pdf")
+ outfile = tmp_path / "temp.pdf"
im.save(outfile)
assert os.path.isfile(outfile)
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index efd2e5cd9..0f0886ab8 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -68,7 +68,7 @@ def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile:
@skip_unless_feature("zlib")
class TestFilePng:
- def get_chunks(self, filename: str) -> list[bytes]:
+ def get_chunks(self, filename: Path) -> list[bytes]:
chunks = []
with open(filename, "rb") as fp:
fp.read(8)
@@ -89,7 +89,7 @@ class TestFilePng:
assert version is not None
assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version)
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
hopper("RGB").save(test_file)
@@ -250,7 +250,7 @@ class TestFilePng:
# each palette entry
assert len(im.info["transparency"]) == 256
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im.save(test_file)
# check if saved image contains same transparency
@@ -271,7 +271,7 @@ class TestFilePng:
assert im.info["transparency"] == 164
assert im.getpixel((31, 31)) == 164
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im.save(test_file)
# check if saved image contains same transparency
@@ -294,7 +294,7 @@ class TestFilePng:
assert im.getcolors() == [(100, (0, 0, 0, 0))]
im = im.convert("P")
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im.save(test_file)
# check if saved image contains same transparency
@@ -315,7 +315,7 @@ class TestFilePng:
im_rgba = im.convert("RGBA")
assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im.save(test_file)
with Image.open(test_file) as test_im:
@@ -329,7 +329,7 @@ class TestFilePng:
def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/caption_6_33_22.png"
with Image.open(in_file) as im:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im.save(test_file)
def test_load_verify(self) -> None:
@@ -488,7 +488,7 @@ class TestFilePng:
im = hopper("P")
im.info["transparency"] = 0
- f = str(tmp_path / "temp.png")
+ f = tmp_path / "temp.png"
im.save(f)
with Image.open(f) as im2:
@@ -549,7 +549,7 @@ class TestFilePng:
def test_chunk_order(self, tmp_path: Path) -> None:
with Image.open("Tests/images/icc_profile.png") as im:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im.convert("P").save(test_file, dpi=(100, 100))
chunks = self.get_chunks(test_file)
@@ -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
@@ -661,7 +667,7 @@ class TestFilePng:
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
im = hopper("P")
- out = str(tmp_path / "temp.png")
+ out = tmp_path / "temp.png"
im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded:
@@ -671,8 +677,8 @@ class TestFilePng:
im = Image.new("P", (1, 1))
im.putpalette((1, 1, 1))
- out = str(tmp_path / "temp.png")
- im.save(str(tmp_path / "temp.png"))
+ out = tmp_path / "temp.png"
+ im.save(out)
with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 3
@@ -721,11 +727,12 @@ class TestFilePng:
def test_exif_save(self, tmp_path: Path) -> None:
# Test exif is not saved from info
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
with Image.open("Tests/images/exif.png") as im:
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
@@ -741,7 +748,7 @@ class TestFilePng:
)
def test_exif_from_jpg(self, tmp_path: Path) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded:
@@ -750,7 +757,7 @@ class TestFilePng:
def test_exif_argument(self, tmp_path: Path) -> None:
with Image.open(TEST_PNG_FILE) as im:
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
im.save(test_file, exif=b"exifstring")
with Image.open(test_file) as reloaded:
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index d87192ca5..41e2b5416 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -94,7 +94,7 @@ def test_16bit_pgm() -> None:
def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im:
- filename = str(tmp_path / "temp.pgm")
+ filename = tmp_path / "temp.pgm"
im.save(filename, "PPM")
assert_image_equal_tofile(im, filename)
@@ -106,7 +106,7 @@ def test_pnm(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.pnm") as im:
assert_image_similar(im, hopper(), 0.0001)
- filename = str(tmp_path / "temp.pnm")
+ filename = tmp_path / "temp.pnm"
im.save(filename)
assert_image_equal_tofile(im, filename)
@@ -117,7 +117,7 @@ def test_pfm(tmp_path: Path) -> None:
assert im.info["scale"] == 1.0
assert_image_equal(im, hopper("F"))
- filename = str(tmp_path / "tmp.pfm")
+ filename = tmp_path / "tmp.pfm"
im.save(filename)
assert_image_equal_tofile(im, filename)
@@ -128,7 +128,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None:
assert im.info["scale"] == 2.5
assert_image_equal(im, hopper("F"))
- filename = str(tmp_path / "tmp.pfm")
+ filename = tmp_path / "tmp.pfm"
im.save(filename)
assert_image_equal_tofile(im, filename)
@@ -194,8 +194,8 @@ def test_16bit_plain_pgm() -> None:
def test_plain_data_with_comment(
tmp_path: Path, header: bytes, data: bytes, comment_count: int
) -> None:
- path1 = str(tmp_path / "temp1.ppm")
- path2 = str(tmp_path / "temp2.ppm")
+ path1 = tmp_path / "temp1.ppm"
+ path2 = tmp_path / "temp2.ppm"
comment = b"# comment" * comment_count
with open(path1, "wb") as f1, open(path2, "wb") as f2:
f1.write(header + b"\n\n" + data)
@@ -207,7 +207,7 @@ def test_plain_data_with_comment(
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(data)
@@ -218,7 +218,7 @@ def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(data)
@@ -235,7 +235,7 @@ def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:
),
)
def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(data)
@@ -245,7 +245,7 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
def test_plain_ppm_value_negative(tmp_path: Path) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n-1")
@@ -255,7 +255,7 @@ def test_plain_ppm_value_negative(tmp_path: Path) -> None:
def test_plain_ppm_value_too_large(tmp_path: Path) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n256")
@@ -270,7 +270,7 @@ def test_magic() -> None:
def test_header_with_comments(tmp_path: Path) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n")
@@ -279,7 +279,7 @@ def test_header_with_comments(tmp_path: Path) -> None:
def test_non_integer_token(tmp_path: Path) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P6\nTEST")
@@ -289,29 +289,25 @@ def test_non_integer_token(tmp_path: Path) -> None:
def test_header_token_too_long(tmp_path: Path) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P6\n 01234567890")
- with pytest.raises(ValueError) as e:
+ with pytest.raises(ValueError, match="Token too long in file header: 01234567890"):
with Image.open(path):
pass
- assert str(e.value) == "Token too long in file header: 01234567890"
-
def test_truncated_file(tmp_path: Path) -> None:
# Test EOF in header
- path = str(tmp_path / "temp.pgm")
+ path = tmp_path / "temp.pgm"
with open(path, "wb") as f:
f.write(b"P6")
- with pytest.raises(ValueError) as e:
+ with pytest.raises(ValueError, match="Reached EOF while reading header"):
with Image.open(path):
pass
- assert str(e.value) == "Reached EOF while reading header"
-
# Test EOF for PyDecoder
fp = BytesIO(b"P5 3 1 4")
with Image.open(fp) as im:
@@ -320,7 +316,7 @@ def test_truncated_file(tmp_path: Path) -> None:
def test_not_enough_image_data(tmp_path: Path) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P2 1 2 255 255")
@@ -331,16 +327,16 @@ def test_not_enough_image_data(tmp_path: Path) -> None:
@pytest.mark.parametrize("maxval", (b"0", b"65536"))
def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:
- path = str(tmp_path / "temp.ppm")
+ path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P6\n3 1 " + maxval)
- with pytest.raises(ValueError) as e:
+ with pytest.raises(
+ ValueError, match="maxval must be greater than 0 and less than 65536"
+ ):
with Image.open(path):
pass
- assert str(e.value) == "maxval must be greater than 0 and less than 65536"
-
def test_neg_ppm() -> None:
# Storage.c accepted negative values for xsize, ysize. the
@@ -354,7 +350,7 @@ def test_neg_ppm() -> None:
def test_mimetypes(tmp_path: Path) -> None:
- path = str(tmp_path / "temp.pgm")
+ path = tmp_path / "temp.pgm"
with open(path, "wb") as f:
f.write(b"P4\n128 128\n255")
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index 1793c269d..38a88cd17 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -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
diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py
index e13a8019e..da0965fa1 100644
--- a/Tests/test_file_sgi.py
+++ b/Tests/test_file_sgi.py
@@ -71,31 +71,33 @@ def test_invalid_file() -> None:
SgiImagePlugin.SgiImageFile(invalid_file)
-def test_write(tmp_path: Path) -> None:
- def roundtrip(img: Image.Image) -> None:
- out = str(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 = str(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:
test_file = "Tests/images/hopper16.rgb"
with Image.open(test_file) as im:
- out = str(tmp_path / "temp.sgi")
+ out = tmp_path / "temp.sgi"
im.save(out, format="sgi", bpc=2)
assert_image_equal_tofile(im, out)
@@ -103,7 +105,7 @@ def test_write16(tmp_path: Path) -> None:
def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("LA")
- out = str(tmp_path / "temp.sgi")
+ out = tmp_path / "temp.sgi"
with pytest.raises(ValueError):
im.save(out, format="sgi")
diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py
index cdb7b3e0b..3b3c3b4a5 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -51,7 +51,7 @@ def test_context_manager() -> None:
def test_save(tmp_path: Path) -> None:
# Arrange
- temp = str(tmp_path / "temp.spider")
+ temp = tmp_path / "temp.spider"
im = hopper()
# Act
@@ -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
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index b6396bd64..8b6ed3ed2 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -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,16 +13,29 @@ _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 = str(tmp_path / "temp.tga")
+ out = tmp_path / "temp.tga"
original_im.save(out, rle=rle)
with Image.open(out) as saved_im:
@@ -36,36 +47,29 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
assert_image_equal(saved_im, original_im)
- 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(tmp_path: Path) -> None:
+def test_palette_depth_8() -> None:
with pytest.raises(UnidentifiedImageError):
Image.open("Tests/images/p_8.tga")
@@ -76,7 +80,7 @@ def test_palette_depth_16(tmp_path: Path) -> None:
assert im.palette.mode == "RGBA"
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
- out = str(tmp_path / "temp.png")
+ out = tmp_path / "temp.png"
im.save(out)
with Image.open(out) as reloaded:
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
@@ -122,7 +126,7 @@ def test_cross_scan_line() -> None:
def test_save(tmp_path: Path) -> None:
test_file = "Tests/images/tga_id_field.tga"
with Image.open(test_file) as im:
- out = str(tmp_path / "temp.tga")
+ out = tmp_path / "temp.tga"
# Save
im.save(out)
@@ -141,7 +145,7 @@ def test_small_palette(tmp_path: Path) -> None:
colors = [0, 0, 0]
im.putpalette(colors)
- out = str(tmp_path / "temp.tga")
+ out = tmp_path / "temp.tga"
im.save(out)
with Image.open(out) as reloaded:
@@ -155,7 +159,7 @@ def test_missing_palette() -> None:
def test_save_wrong_mode(tmp_path: Path) -> None:
im = hopper("PA")
- out = str(tmp_path / "temp.tga")
+ out = tmp_path / "temp.tga"
with pytest.raises(OSError):
im.save(out)
@@ -172,7 +176,7 @@ def test_save_mapdepth() -> None:
def test_save_id_section(tmp_path: Path) -> None:
test_file = "Tests/images/rgb32rle.tga"
with Image.open(test_file) as im:
- out = str(tmp_path / "temp.tga")
+ out = tmp_path / "temp.tga"
# Check there is no id section
im.save(out)
@@ -202,7 +206,7 @@ def test_save_id_section(tmp_path: Path) -> None:
def test_save_orientation(tmp_path: Path) -> None:
test_file = "Tests/images/rgb32rle.tga"
- out = str(tmp_path / "temp.tga")
+ out = tmp_path / "temp.tga"
with Image.open(test_file) as im:
assert im.info["orientation"] == -1
@@ -229,7 +233,7 @@ def test_save_rle(tmp_path: Path) -> None:
with Image.open(test_file) as im:
assert im.info["compression"] == "tga_rle"
- out = str(tmp_path / "temp.tga")
+ out = tmp_path / "temp.tga"
# Save
im.save(out)
@@ -266,7 +270,7 @@ def test_save_l_transparency(tmp_path: Path) -> None:
assert im.mode == "LA"
assert im.getchannel("A").getcolors()[0][0] == num_transparent
- out = str(tmp_path / "temp.tga")
+ out = tmp_path / "temp.tga"
im.save(out)
with Image.open(out) as test_im:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index a8a407963..502d9df9a 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -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 (
@@ -31,7 +37,7 @@ except ImportError:
class TestFileTiff:
def test_sanity(self, tmp_path: Path) -> None:
- filename = str(tmp_path / "temp.tif")
+ filename = tmp_path / "temp.tif"
hopper("RGB").save(filename)
@@ -112,20 +118,23 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_bigtiff_save(self, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
im = hopper()
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:
@@ -134,13 +143,14 @@ class TestFileTiff:
def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
- with pytest.raises(Exception) as e:
+ with pytest.raises(Exception, match="Not allowing setting of legacy api"):
ifd.legacy_api = False
- assert str(e.value) == "Not allowing setting of legacy api"
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)
@@ -154,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)
@@ -168,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
@@ -182,11 +196,12 @@ 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)
def test_save_float_dpi(self, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/hopper.tif") as im:
dpi = (72.2, 72.2)
im.save(outfile, dpi=dpi)
@@ -199,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
@@ -214,19 +230,21 @@ 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")
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
im.save(outfile)
def test_save_unsupported_mode(self, tmp_path: Path) -> None:
im = hopper("HSV")
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
with pytest.raises(OSError):
im.save(outfile)
@@ -308,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
@@ -356,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)
@@ -379,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,
@@ -418,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))
@@ -450,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:
@@ -486,14 +515,14 @@ class TestFileTiff:
assert gps[0] == b"\x03\x02\x00\x00"
assert gps[18] == "WGS-84"
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
check_exif(exif)
im.save(outfile, exif=exif)
- outfile2 = str(tmp_path / "temp2.tif")
+ outfile2 = tmp_path / "temp2.tif"
with Image.open(outfile) as im:
exif = im.getexif()
check_exif(exif)
@@ -505,7 +534,7 @@ class TestFileTiff:
check_exif(exif)
def test_modify_exif(self, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
exif[264] = 100
@@ -534,10 +563,11 @@ class TestFileTiff:
@pytest.mark.parametrize("mode", ("1", "L"))
def test_photometric(self, mode: str, tmp_path: Path) -> None:
- filename = str(tmp_path / "temp.tif")
+ filename = tmp_path / "temp.tif"
im = hopper(mode)
im.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)
@@ -613,9 +643,11 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
- filename = str(tmp_path / "temp.tif")
+ filename = tmp_path / "temp.tif"
hopper("RGB").save(filename, "TIFF", **kwargs)
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
@@ -631,14 +663,14 @@ class TestFileTiff:
with Image.open(infile) as im:
assert im.getpixel((0, 0)) == pixel_value
- tmpfile = str(tmp_path / "temp.tif")
+ tmpfile = tmp_path / "temp.tif"
im.save(tmpfile)
assert_image_equal_tofile(im, tmpfile)
def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/hopper.tif") as im:
im.load()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
@@ -653,7 +685,7 @@ class TestFileTiff:
assert 33723 not in im.tag_v2
def test_rowsperstrip(self, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
im = hopper()
im.save(outfile, tiffinfo={278: 256})
@@ -661,6 +693,18 @@ class TestFileTiff:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[278] == 256
+ im = hopper()
+ im2 = Image.new("L", (128, 128))
+ im2.encoderinfo = {"tiffinfo": {278: 256}}
+ im.save(outfile, save_all=True, append_images=[im2])
+
+ with Image.open(outfile) as im:
+ assert isinstance(im, TiffImagePlugin.TiffImageFile)
+ assert im.tag_v2[278] == 128
+
+ im.seek(1)
+ assert im.tag_v2[278] == 256
+
def test_strip_raw(self) -> None:
infile = "Tests/images/tiff_strip_raw.tif"
with Image.open(infile) as im:
@@ -690,9 +734,10 @@ 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 = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
im.save(outfile)
with Image.open(outfile) as reloaded:
@@ -707,7 +752,7 @@ class TestFileTiff:
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
im = hopper(mode)
im.save(outfile)
@@ -722,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
@@ -732,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
@@ -743,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_fixoffsets(self) -> None:
@@ -801,7 +849,7 @@ class TestFileTiff:
im.info["icc_profile"] = "Dummy value"
# Try save-load round trip to make sure both handle icc_profile.
- tmpfile = str(tmp_path / "temp.tif")
+ tmpfile = tmp_path / "temp.tif"
im.save(tmpfile, "TIFF", compression="raw")
with Image.open(tmpfile) as reloaded:
assert b"Dummy value" == reloaded.info["icc_profile"]
@@ -810,7 +858,7 @@ class TestFileTiff:
im = hopper()
assert "icc_profile" not in im.info
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
icc_profile = b"Dummy value"
im.save(outfile, icc_profile=icc_profile)
@@ -821,11 +869,11 @@ class TestFileTiff:
with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["compression"] == 0
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
im.save(outfile)
def test_discard_icc_profile(self, tmp_path: Path) -> None:
- outfile = str(tmp_path / "temp.tif")
+ outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/icc_profile.png") as im:
assert "icc_profile" in im.info
@@ -853,6 +901,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,
@@ -878,7 +927,7 @@ class TestFileTiff:
]
def test_tiff_chunks(self, tmp_path: Path) -> None:
- tmpfile = str(tmp_path / "temp.tif")
+ tmpfile = tmp_path / "temp.tif"
im = hopper()
with open(tmpfile, "wb") as fp:
@@ -900,7 +949,7 @@ class TestFileTiff:
def test_close_on_load_exclusive(self, tmp_path: Path) -> None:
# similar to test_fd_leak, but runs on unixlike os
- tmpfile = str(tmp_path / "temp.tif")
+ tmpfile = tmp_path / "temp.tif"
with Image.open("Tests/images/uint16_1_4660.tif") as im:
im.save(tmpfile)
@@ -912,7 +961,7 @@ class TestFileTiff:
assert fp.closed
def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None:
- tmpfile = str(tmp_path / "temp.tif")
+ tmpfile = tmp_path / "temp.tif"
with Image.open("Tests/images/uint16_1_4660.tif") as im:
im.save(tmpfile)
@@ -963,7 +1012,7 @@ class TestFileTiff:
@pytest.mark.skipif(not is_win32(), reason="Windows only")
class TestFileTiffW32:
def test_fd_leak(self, tmp_path: Path) -> None:
- tmpfile = str(tmp_path / "temp.tif")
+ tmpfile = tmp_path / "temp.tif"
# this is an mmaped file.
with Image.open("Tests/images/uint16_1_4660.tif") as im:
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index 36aabf4f8..884868345 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -56,11 +56,12 @@ def test_rt_metadata(tmp_path: Path) -> None:
info[ImageDescription] = text_data
- f = str(tmp_path / "temp.tif")
+ f = tmp_path / "temp.tif"
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,13 +131,15 @@ 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:
- f = str(tmp_path / "temp.tiff")
+ assert isinstance(img, TiffImagePlugin.TiffImageFile)
+ f = tmp_path / "temp.tiff"
del img.tag[278]
img.save(f, tiffinfo=img.tag)
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"]
@@ -163,8 +168,9 @@ def test_write_metadata(tmp_path: Path) -> None:
def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper.tif") as im:
+ 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
@@ -210,7 +217,7 @@ def test_no_duplicate_50741_tag() -> None:
def test_iptc(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper.Lab.tif") as im:
im.save(out)
@@ -227,10 +234,11 @@ def test_writing_other_types_to_ascii(
info[271] = value
im = hopper()
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
+ assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[271] == expected
@@ -244,10 +252,11 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path)
info[700] = value
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
+ assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[700] == b"\x01"
@@ -263,10 +272,11 @@ def test_writing_other_types_to_undefined(
info[33723] = value
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
+ assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[33723] == b"1"
@@ -296,7 +306,7 @@ def test_empty_metadata() -> None:
def test_iccprofile(tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/1462
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
im.save(out)
@@ -311,19 +321,20 @@ 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"]
def test_iccprofile_save_png(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
- outfile = str(tmp_path / "temp.png")
+ outfile = tmp_path / "temp.png"
im.save(outfile)
def test_iccprofile_binary_save_png(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
- outfile = str(tmp_path / "temp.png")
+ outfile = tmp_path / "temp.png"
im.save(outfile)
@@ -332,10 +343,11 @@ def test_exif_div_zero(tmp_path: Path) -> None:
info = TiffImagePlugin.ImageFileDirectory_v2()
info[41988] = TiffImagePlugin.IFDRational(0, 0)
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
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
@@ -351,10 +363,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
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
@@ -363,10 +376,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
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
@@ -381,10 +395,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
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
@@ -393,10 +408,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
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
@@ -406,10 +422,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
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
@@ -420,10 +437,11 @@ def test_ifd_signed_long(tmp_path: Path) -> None:
info[37000] = -60000
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
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 = str(tmp_path / "temp.tiff")
+ 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)
@@ -480,7 +500,7 @@ def test_tag_group_data() -> None:
def test_empty_subifd(tmp_path: Path) -> None:
- out = str(tmp_path / "temp.jpg")
+ out = tmp_path / "temp.jpg"
im = hopper()
exif = im.getexif()
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
index abe888241..f61e2c82e 100644
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -154,9 +154,8 @@ class TestFileWebp:
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
im = Image.new("RGB", (15000, 15000))
- with pytest.raises(ValueError) as e:
+ with pytest.raises(ValueError, match="encoding error 6"):
im.save(tmp_path / "temp.webp", method=0)
- assert str(e.value) == "encoding error 6"
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
@@ -231,7 +230,7 @@ class TestFileWebp:
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(0, 3))
+ difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3))
assert difference < 5
def test_duration(self, tmp_path: Path) -> None:
diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py
index c88fe3589..c573390c4 100644
--- a/Tests/test_file_webp_alpha.py
+++ b/Tests/test_file_webp_alpha.py
@@ -42,7 +42,7 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
Does it have the bits we expect?
"""
- temp_file = str(tmp_path / "temp.webp")
+ temp_file = tmp_path / "temp.webp"
# temp_file = "temp.webp"
pil_image = hopper("RGBA")
@@ -71,7 +71,7 @@ def test_write_rgba(tmp_path: Path) -> None:
Does it have the bits we expect?
"""
- temp_file = str(tmp_path / "temp.webp")
+ temp_file = tmp_path / "temp.webp"
pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20))
pil_image.save(temp_file)
@@ -104,7 +104,7 @@ def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None:
half_transparent_image.putalpha(new_alpha)
# save with transparent area preserved
- temp_file = str(tmp_path / "temp.webp")
+ temp_file = tmp_path / "temp.webp"
half_transparent_image.save(temp_file, exact=True, lossless=True)
with Image.open(temp_file) as reloaded:
@@ -123,7 +123,7 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
should work, and be similar to the original file.
"""
- temp_file = str(tmp_path / "temp.webp")
+ temp_file = tmp_path / "temp.webp"
file_path = "Tests/images/transparent.gif"
with Image.open(file_path) as im:
im.save(temp_file)
@@ -142,10 +142,10 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
def test_alpha_quality(tmp_path: Path) -> None:
with Image.open("Tests/images/transparent.png") as im:
- out = str(tmp_path / "temp.webp")
+ out = tmp_path / "temp.webp"
im.save(out)
- out_quality = str(tmp_path / "quality.webp")
+ out_quality = tmp_path / "quality.webp"
im.save(out_quality, alpha_quality=50)
with Image.open(out) as reloaded:
with Image.open(out_quality) as reloaded_quality:
diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py
index 967a0aae8..503761374 100644
--- a/Tests/test_file_webp_animated.py
+++ b/Tests/test_file_webp_animated.py
@@ -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 = str(tmp_path / "temp.webp")
+ 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
@@ -67,8 +71,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
are visually similar to the originals.
"""
- def check(temp_file: str) -> None:
+ def check(temp_file: Path) -> None:
with Image.open(temp_file) as im:
+ assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 2
# Compare first frame to original
@@ -87,7 +92,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
with Image.open("Tests/images/anim_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2:
- temp_file1 = str(tmp_path / "temp.webp")
+ temp_file1 = tmp_path / "temp.webp"
frame1.copy().save(
temp_file1, save_all=True, append_images=[frame2], lossless=True
)
@@ -99,7 +104,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
) -> Generator[Image.Image, None, None]:
yield from ims
- temp_file2 = str(tmp_path / "temp_generator.webp")
+ temp_file2 = tmp_path / "temp_generator.webp"
frame1.copy().save(
temp_file2,
save_all=True,
@@ -116,7 +121,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
"""
durations = [0, 10, 20, 30, 40]
- temp_file = str(tmp_path / "temp.webp")
+ temp_file = tmp_path / "temp.webp"
with Image.open("Tests/images/anim_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2:
frame1.save(
@@ -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
@@ -141,7 +147,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
def test_float_duration(tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.webp")
+ temp_file = tmp_path / "temp.webp"
with Image.open("Tests/images/iss634.apng") as im:
assert im.info["duration"] == 70.0
@@ -159,7 +165,7 @@ def test_seeking(tmp_path: Path) -> None:
"""
dur = 33
- temp_file = str(tmp_path / "temp.webp")
+ temp_file = tmp_path / "temp.webp"
with Image.open("Tests/images/anim_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2:
frame1.save(
@@ -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
@@ -196,10 +203,10 @@ def test_alpha_quality(tmp_path: Path) -> None:
with Image.open("Tests/images/transparent.png") as im:
first_frame = Image.new("L", im.size)
- out = str(tmp_path / "temp.webp")
+ out = tmp_path / "temp.webp"
first_frame.save(out, save_all=True, append_images=[im])
- out_quality = str(tmp_path / "quality.webp")
+ out_quality = tmp_path / "quality.webp"
first_frame.save(
out_quality, save_all=True, append_images=[im], alpha_quality=50
)
diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py
index 80429715e..5eaa4f599 100644
--- a/Tests/test_file_webp_lossless.py
+++ b/Tests/test_file_webp_lossless.py
@@ -13,7 +13,7 @@ RGB_MODE = "RGB"
def test_write_lossless_rgb(tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.webp")
+ temp_file = tmp_path / "temp.webp"
hopper(RGB_MODE).save(temp_file, lossless=True)
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index c68a20d7a..7543d22da 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -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()
@@ -146,7 +147,7 @@ def test_write_animated_metadata(tmp_path: Path) -> None:
exif_data = b""
xmp_data = b""
- temp_file = str(tmp_path / "temp.webp")
+ temp_file = tmp_path / "temp.webp"
with Image.open("Tests/images/anim_frame1.webp") as frame1:
with Image.open("Tests/images/anim_frame2.webp") as frame2:
frame1.save(
diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py
index 97469b77e..dcf5f000f 100644
--- a/Tests/test_file_wmf.py
+++ b/Tests/test_file_wmf.py
@@ -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
@@ -59,7 +68,7 @@ def test_register_handler(tmp_path: Path) -> None:
WmfImagePlugin.register_handler(handler)
im = hopper()
- tmpfile = str(tmp_path / "temp.wmf")
+ tmpfile = tmp_path / "temp.wmf"
im.save(tmpfile)
assert handler.methodCalled
@@ -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,11 +98,27 @@ 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:
im = hopper()
- tmpfile = str(tmp_path / ("temp" + ext))
+ tmpfile = tmp_path / ("temp" + ext)
with pytest.raises(OSError):
im.save(tmpfile)
diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py
index 44dd2541f..154f3dcc0 100644
--- a/Tests/test_file_xbm.py
+++ b/Tests/test_file_xbm.py
@@ -73,7 +73,7 @@ def test_invalid_file() -> None:
def test_save_wrong_mode(tmp_path: Path) -> None:
im = hopper()
- out = str(tmp_path / "temp.xbm")
+ out = tmp_path / "temp.xbm"
with pytest.raises(OSError):
im.save(out)
@@ -81,7 +81,7 @@ def test_save_wrong_mode(tmp_path: Path) -> None:
def test_hotspot(tmp_path: Path) -> None:
im = hopper("1")
- out = str(tmp_path / "temp.xbm")
+ out = tmp_path / "temp.xbm"
hotspot = (0, 7)
im.save(out, hotspot=hotspot)
diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py
index 26afe93f4..73c62a44d 100644
--- a/Tests/test_file_xpm.py
+++ b/Tests/test_file_xpm.py
@@ -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
diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py
index 2ece5457a..b4155c879 100644
--- a/Tests/test_font_bdf.py
+++ b/Tests/test_font_bdf.py
@@ -20,8 +20,8 @@ def test_sanity() -> None:
def test_zero_width_chars() -> None:
with open(filename, "rb") as fp:
data = fp.read()
- data = data[:2650] + b"\x00\x00" + data[2652:]
- BdfFontFile.BdfFontFile(io.BytesIO(data))
+ data = data[:2650] + b"\x00\x00" + data[2652:]
+ BdfFontFile.BdfFontFile(io.BytesIO(data))
def test_invalid_file() -> None:
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 5474f951c..7e6118d52 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -65,9 +65,8 @@ class TestImage:
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None:
- with pytest.raises(ValueError) as e:
+ with pytest.raises(ValueError, match="unrecognized image mode"):
Image.new(mode, (1, 1))
- assert str(e.value) == "unrecognized image mode"
def test_exception_inheritance(self) -> None:
assert issubclass(UnidentifiedImageError, OSError)
@@ -176,6 +175,13 @@ class TestImage:
with Image.open(io.StringIO()): # type: ignore[arg-type]
pass
+ def test_string(self, tmp_path: Path) -> None:
+ out = str(tmp_path / "temp.png")
+ im = hopper()
+ im.save(out)
+ with Image.open(out) as reloaded:
+ assert_image_equal(im, reloaded)
+
def test_pathlib(self, tmp_path: Path) -> None:
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
assert im.mode == "P"
@@ -188,14 +194,13 @@ class TestImage:
for ext in (".jpg", ".jp2"):
if ext == ".jp2" and not features.check_codec("jpg_2000"):
pytest.skip("jpg_2000 not available")
- temp_file = str(tmp_path / ("temp." + ext))
- im.save(Path(temp_file))
+ im.save(tmp_path / ("temp." + ext))
def test_fp_name(self, tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.jpg")
+ temp_file = tmp_path / "temp.jpg"
class FP(io.BytesIO):
- name: str
+ name: Path
if sys.version_info >= (3, 12):
from collections.abc import Buffer
@@ -225,10 +230,10 @@ class TestImage:
assert_image_similar(im, reloaded, 20)
def test_unknown_extension(self, tmp_path: Path) -> None:
- im = hopper()
- temp_file = str(tmp_path / "temp.unknown")
- with pytest.raises(ValueError):
- im.save(temp_file)
+ temp_file = tmp_path / "temp.unknown"
+ with hopper() as im:
+ with pytest.raises(ValueError):
+ im.save(temp_file)
def test_internals(self) -> None:
im = Image.new("L", (100, 100))
@@ -246,13 +251,22 @@ class TestImage:
reason="Test requires opening an mmaped file for writing",
)
def test_readonly_save(self, tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.bmp")
+ temp_file = tmp_path / "temp.bmp"
shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
with Image.open(temp_file) as im:
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"))
@@ -729,7 +743,7 @@ class TestImage:
# https://github.com/python-pillow/Pillow/issues/835
# Arrange
test_file = "Tests/images/hopper.png"
- temp_file = str(tmp_path / "temp.jpg")
+ temp_file = tmp_path / "temp.jpg"
# Act/Assert
with Image.open(test_file) as im:
@@ -739,7 +753,7 @@ class TestImage:
im.save(temp_file)
def test_no_new_file_on_error(self, tmp_path: Path) -> None:
- temp_file = str(tmp_path / "temp.jpg")
+ temp_file = tmp_path / "temp.jpg"
im = Image.new("RGB", (0, 0))
with pytest.raises(ValueError):
@@ -806,7 +820,7 @@ class TestImage:
assert exif[296] == 2
assert exif[11] == "gThumb 3.0.1"
- out = str(tmp_path / "temp.jpg")
+ out = tmp_path / "temp.jpg"
exif[258] = 8
del exif[274]
del exif[282]
@@ -828,7 +842,7 @@ class TestImage:
assert exif[274] == 1
assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)"
- out = str(tmp_path / "temp.jpg")
+ out = tmp_path / "temp.jpg"
exif[258] = 8
del exif[306]
exif[274] = 455
@@ -847,7 +861,7 @@ class TestImage:
exif = im.getexif()
assert exif == {}
- out = str(tmp_path / "temp.webp")
+ out = tmp_path / "temp.webp"
exif[258] = 8
exif[40963] = 455
exif[305] = "Pillow test"
@@ -869,7 +883,7 @@ class TestImage:
exif = im.getexif()
assert exif == {274: 1}
- out = str(tmp_path / "temp.png")
+ out = tmp_path / "temp.png"
exif[258] = 8
del exif[274]
exif[40963] = 455
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 5f8b35c79..7d4f78c23 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -118,7 +118,7 @@ def test_trns_p(tmp_path: Path) -> None:
im = hopper("P")
im.info["transparency"] = 0
- f = str(tmp_path / "temp.png")
+ f = tmp_path / "temp.png"
im_l = im.convert("L")
assert im_l.info["transparency"] == 0
@@ -154,7 +154,7 @@ def test_trns_l(tmp_path: Path) -> None:
im = hopper("L")
im.info["transparency"] = 128
- f = str(tmp_path / "temp.png")
+ f = tmp_path / "temp.png"
im_la = im.convert("LA")
assert "transparency" not in im_la.info
@@ -177,7 +177,7 @@ def test_trns_RGB(tmp_path: Path) -> None:
im = hopper("RGB")
im.info["transparency"] = im.getpixel((0, 0))
- f = str(tmp_path / "temp.png")
+ f = tmp_path / "temp.png"
im_l = im.convert("L")
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index 1166371b8..f700d20c0 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -171,7 +171,7 @@ class TestImagingCoreResize:
# platforms. So if a future Pillow change requires that the test file
# be updated, that is okay.
im = hopper().resize((64, 64))
- temp_file = str(tmp_path / "temp.gif")
+ temp_file = tmp_path / "temp.gif"
im.save(temp_file)
with Image.open(temp_file) as reloaded:
diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py
index 3385f81f5..43068535e 100644
--- a/Tests/test_image_split.py
+++ b/Tests/test_image_split.py
@@ -45,9 +45,9 @@ def test_split_merge(mode: str) -> None:
def test_split_open(tmp_path: Path) -> None:
if features.check("zlib"):
- test_file = str(tmp_path / "temp.png")
+ test_file = tmp_path / "temp.png"
else:
- test_file = str(tmp_path / "temp.pcx")
+ test_file = tmp_path / "temp.pcx"
def split_open(mode: str) -> int:
hopper(mode).save(test_file)
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 232cbb16c..ffe9c0979 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -39,6 +39,8 @@ BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X
POINTS = (
((10, 10), (20, 40), (30, 30)),
[(10, 10), (20, 40), (30, 30)],
+ ([10, 10], [20, 40], [30, 30]),
+ [[10, 10], [20, 40], [30, 30]],
(10, 10, 20, 40, 30, 30),
[10, 10, 20, 40, 30, 30],
)
@@ -46,6 +48,8 @@ POINTS = (
KITE_POINTS = (
((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)),
[(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)],
+ ([10, 50], [70, 10], [90, 50], [70, 90], [10, 50]),
+ [[10, 50], [70, 10], [90, 50], [70, 90], [10, 50]],
)
@@ -1044,8 +1048,8 @@ def create_base_image_draw(
background2: tuple[int, int, int] = GRAY,
) -> tuple[Image.Image, ImageDraw.ImageDraw]:
img = Image.new(mode, size, background1)
- for x in range(0, size[0]):
- for y in range(0, size[1]):
+ for x in range(size[0]):
+ for y in range(size[1]):
if (x + y) % 2 == 0:
img.putpixel((x, y), background2)
return img, ImageDraw.Draw(img)
@@ -1626,7 +1630,7 @@ def test_compute_regular_polygon_vertices(
0,
ValueError,
"bounding_circle should contain 2D coordinates "
- "and a radius (e.g. (x, y, r) or ((x, y), r) )",
+ r"and a radius \(e.g. \(x, y, r\) or \(\(x, y\), r\) \)",
),
(
3,
@@ -1640,7 +1644,7 @@ def test_compute_regular_polygon_vertices(
((50, 50, 50), 25),
0,
ValueError,
- "bounding_circle centre should contain 2D coordinates (e.g. (x, y))",
+ r"bounding_circle centre should contain 2D coordinates \(e.g. \(x, y\)\)",
),
(
3,
@@ -1665,9 +1669,8 @@ def test_compute_regular_polygon_vertices_input_error_handling(
expected_error: type[Exception],
error_message: str,
) -> None:
- with pytest.raises(expected_error) as e:
+ with pytest.raises(expected_error, match=error_message):
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
- assert str(e.value) == error_message
def test_continuous_horizontal_edges_polygon() -> None:
@@ -1701,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:
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index b05d29dae..7622eea99 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -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):
@@ -176,9 +196,8 @@ class TestImageFile:
b"0" * ImageFile.SAFEBLOCK
) # only SAFEBLOCK bytes, so that the header is truncated
)
- with pytest.raises(OSError) as e:
+ with pytest.raises(OSError, match="Truncated File Read"):
BmpImagePlugin.BmpImageFile(b)
- assert str(e.value) == "Truncated File Read"
@skip_unless_feature("zlib")
def test_truncated_with_errors(self) -> None:
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 4cce8f180..69533c2f8 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -124,7 +124,7 @@ def test_render_equal(layout_engine: ImageFont.Layout) -> None:
def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None:
- tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
+ tempfile = tmp_path / ("temp_" + chr(128) + ".ttf")
try:
shutil.copy(FONT_PATH, tempfile)
except UnicodeEncodeError:
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index 5cd510751..01fa090dc 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -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"])
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index 6180a7b5d..515e29cea 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -80,15 +80,12 @@ def test_lut(op: str) -> None:
def test_no_operator_loaded() -> None:
im = Image.new("L", (1, 1))
mop = ImageMorph.MorphOp()
- with pytest.raises(Exception) as e:
+ with pytest.raises(Exception, match="No operator loaded"):
mop.apply(im)
- assert str(e.value) == "No operator loaded"
- with pytest.raises(Exception) as e:
+ with pytest.raises(Exception, match="No operator loaded"):
mop.match(im)
- assert str(e.value) == "No operator loaded"
- with pytest.raises(Exception) as e:
+ with pytest.raises(Exception, match="No operator loaded"):
mop.save_lut("")
- assert str(e.value) == "No operator loaded"
# Test the named patterns
@@ -238,15 +235,12 @@ def test_incorrect_mode() -> None:
im = hopper("RGB")
mop = ImageMorph.MorphOp(op_name="erosion8")
- with pytest.raises(ValueError) as e:
+ with pytest.raises(ValueError, match="Image mode must be L"):
mop.apply(im)
- assert str(e.value) == "Image mode must be L"
- with pytest.raises(ValueError) as e:
+ with pytest.raises(ValueError, match="Image mode must be L"):
mop.match(im)
- assert str(e.value) == "Image mode must be L"
- with pytest.raises(ValueError) as e:
+ with pytest.raises(ValueError, match="Image mode must be L"):
mop.get_on_pixels(im)
- assert str(e.value) == "Image mode must be L"
def test_add_patterns() -> None:
@@ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None:
lb.add_patterns(new_patterns)
# Act / Assert
- with pytest.raises(Exception) as e:
+ with pytest.raises(
+ Exception, match='Syntax error in pattern "a pattern with a syntax error"'
+ ):
lb.build_lut()
- assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"'
def test_load_invalid_mrl() -> None:
@@ -290,9 +285,8 @@ def test_load_invalid_mrl() -> None:
mop = ImageMorph.MorphOp()
# Act / Assert
- with pytest.raises(Exception) as e:
+ with pytest.raises(Exception, match="Wrong size operator file!"):
mop.load_lut(invalid_mrl)
- assert str(e.value) == "Wrong size operator file!"
def test_roundtrip_mrl(tmp_path: Path) -> None:
diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py
index e0b6359b0..782022f51 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -112,7 +112,7 @@ def test_make_linear_lut() -> None:
assert isinstance(lut, list)
assert len(lut) == 256
# Check values
- for i in range(0, len(lut)):
+ for i in range(len(lut)):
assert lut[i] == i
diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py
index 1b1ee6bac..ad8acde49 100644
--- a/Tests/test_imagepath.py
+++ b/Tests/test_imagepath.py
@@ -68,25 +68,10 @@ def test_path_constructors(
assert list(p) == [(0.0, 1.0)]
-@pytest.mark.parametrize(
- "coords",
- (
- ("a", "b"),
- ([0, 1],),
- [[0, 1]],
- ([0.0, 1.0],),
- [[0.0, 1.0]],
- ),
-)
-def test_invalid_path_constructors(
- coords: tuple[str, str] | Sequence[Sequence[int]],
-) -> None:
- # Act
- with pytest.raises(ValueError) as e:
- ImagePath.Path(coords)
-
- # Assert
- assert str(e.value) == "incorrect coordinate type"
+def test_invalid_path_constructors() -> None:
+ # Arrange / Act
+ with pytest.raises(ValueError, match="incorrect coordinate type"):
+ ImagePath.Path(("a", "b"))
@pytest.mark.parametrize(
@@ -99,13 +84,9 @@ def test_invalid_path_constructors(
),
)
def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None:
- # Act
- with pytest.raises(ValueError) as e:
+ with pytest.raises(ValueError, match="wrong number of coordinates"):
ImagePath.Path(coords)
- # Assert
- assert str(e.value) == "wrong number of coordinates"
-
@pytest.mark.parametrize(
"coords, expected",
diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py
index 9b37435eb..7b9ac80bc 100644
--- a/Tests/test_imagesequence.py
+++ b/Tests/test_imagesequence.py
@@ -4,13 +4,13 @@ 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
def test_sanity(tmp_path: Path) -> None:
- test_file = str(tmp_path / "temp.im")
+ test_file = tmp_path / "temp.im"
im = hopper("RGB")
im.save(test_file)
@@ -31,8 +31,9 @@ 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(0, im.n_frames):
+ for index in range(im.n_frames):
assert i[index] == next(i)
with pytest.raises(IndexError):
i[index + 1]
@@ -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)
diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py
index c23a5c690..e8468e59f 100644
--- a/Tests/test_imagewin_pointers.py
+++ b/Tests/test_imagewin_pointers.py
@@ -88,7 +88,7 @@ if is_win32():
def test_pointer(tmp_path: Path) -> None:
im = hopper()
(width, height) = im.size
- opath = str(tmp_path / "temp.png")
+ opath = tmp_path / "temp.png"
imdib = ImageWin.Dib(im)
hdr = BITMAPINFOHEADER()
diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py
index e26f5d283..b78b7984f 100644
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -44,7 +44,7 @@ def test_basic(tmp_path: Path, mode: str) -> None:
im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(im_out) # transform
- filename = str(tmp_path / "temp.im")
+ filename = tmp_path / "temp.im"
im_in.save(filename)
with Image.open(filename) as im_out:
diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py
index c4f8de013..1c48cb743 100644
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -18,7 +18,7 @@ def helper_pickle_file(
) -> None:
# Arrange
with Image.open(test_file) as im:
- filename = str(tmp_path / "temp.pkl")
+ filename = tmp_path / "temp.pkl"
if mode:
im = im.convert(mode)
@@ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
("Tests/images/itxt_chunks.png", None),
],
)
-@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1))
+@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_image(
tmp_path: Path, test_file: str, test_mode: str | None, protocol: int
) -> None:
@@ -81,18 +81,19 @@ 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
def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange
- filename = str(tmp_path / "temp.pkl")
+ filename = tmp_path / "temp.pkl"
with Image.open("Tests/images/hopper.jpg") as im:
im = im.convert("PA")
# Act / Assert
- for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
+ for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
im._mode = "LA"
with open(filename, "wb") as f:
pickle.dump(im, f, protocol)
@@ -133,7 +134,7 @@ def helper_assert_pickled_font_images(
@skip_unless_feature("freetype2")
-@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
+@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_string(protocol: int) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
@@ -147,11 +148,11 @@ def test_pickle_font_string(protocol: int) -> None:
@skip_unless_feature("freetype2")
-@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
+@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_file(tmp_path: Path, protocol: int) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
- filename = str(tmp_path / "temp.pkl")
+ filename = tmp_path / "temp.pkl"
# Act: roundtrip
with open(filename, "wb") as f:
diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py
index a743d831f..78f5632c5 100644
--- a/Tests/test_psdraw.py
+++ b/Tests/test_psdraw.py
@@ -35,7 +35,7 @@ def test_draw_postscript(tmp_path: Path) -> None:
# https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript
# Arrange
- tempfile = str(tmp_path / "temp.ps")
+ tempfile = tmp_path / "temp.ps"
with open(tempfile, "wb") as fp:
# Act
ps = PSDraw.PSDraw(fp)
diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py
new file mode 100644
index 000000000..ece9f8f26
--- /dev/null
+++ b/Tests/test_pyarrow.py
@@ -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)
diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py
index c2f7fe22e..8235daf32 100644
--- a/Tests/test_pyroma.py
+++ b/Tests/test_pyroma.py
@@ -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."
+ ],
+ )
diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py
index dd4fc46c3..03e92b5b9 100644
--- a/Tests/test_shell_injection.py
+++ b/Tests/test_shell_injection.py
@@ -35,10 +35,11 @@ class TestShellInjection:
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg_filename(self, tmp_path: Path) -> None:
for filename in test_filenames:
- src_file = str(tmp_path / filename)
+ src_file = tmp_path / filename
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")
diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py
index 13f1f9c80..42d06b896 100644
--- a/Tests/test_tiff_ifdrational.py
+++ b/Tests/test_tiff_ifdrational.py
@@ -65,11 +65,12 @@ def test_ifd_rational_save(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None:
im = hopper()
- out = str(tmp_path / "temp.tiff")
+ out = tmp_path / "temp.tiff"
res = IFDRational(301, 1)
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
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])
diff --git a/depends/install_libavif.sh b/depends/install_libavif.sh
new file mode 100755
index 000000000..fc10d3e54
--- /dev/null
+++ b/depends/install_libavif.sh
@@ -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
diff --git a/docs/Guardfile b/docs/Guardfile
deleted file mode 100755
index 16a891a73..000000000
--- a/docs/Guardfile
+++ /dev/null
@@ -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"))
diff --git a/docs/Makefile b/docs/Makefile
index e90af0519..1e6c06ede 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -20,26 +20,11 @@ help:
@echo "Please use \`make ' where 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"
- @echo " json to make JSON files"
- @echo " htmlhelp to make HTML files and a HTML help project"
- @echo " qthelp to make HTML files and a qthelp project"
- @echo " devhelp to make HTML files and a Devhelp project"
- @echo " epub to make an epub"
- @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
- @echo " latexpdf to make LaTeX files and run them through pdflatex"
- @echo " text to make text files"
- @echo " man to make manual pages"
- @echo " texinfo to make Texinfo files"
- @echo " info to make Texinfo files and run them through makeinfo"
- @echo " gettext to make PO message catalogs"
- @echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
- @echo " doctest to run all doctests embedded in the documentation (if enabled)"
.PHONY: clean
clean:
@@ -69,119 +54,6 @@ singlehtml:
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-.PHONY: pickle
-pickle:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
- @echo
- @echo "Build finished; now you can process the pickle files."
-
-.PHONY: json
-json:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder json $(ALLSPHINXOPTS) $(BUILDDIR)/json
- @echo
- @echo "Build finished; now you can process the JSON files."
-
-.PHONY: htmlhelp
-htmlhelp:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
- @echo
- @echo "Build finished; now you can run HTML Help Workshop with the" \
- ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-.PHONY: qthelp
-qthelp:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
- @echo
- @echo "Build finished; now you can run "qcollectiongenerator" with the" \
- ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
- @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PillowPILfork.qhcp"
- @echo "To view the help file:"
- @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc"
-
-.PHONY: devhelp
-devhelp:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
- @echo
- @echo "Build finished."
- @echo "To view the help file:"
- @echo "# mkdir -p $$HOME/.local/share/devhelp/PillowPILfork"
- @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork"
- @echo "# devhelp"
-
-.PHONY: epub
-epub:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
- @echo
- @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-.PHONY: latex
-latex:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo
- @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
- @echo "Run \`make' in that directory to run these through (pdf)latex" \
- "(use \`make latexpdf' here to do that automatically)."
-
-.PHONY: latexpdf
-latexpdf:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo "Running LaTeX files through pdflatex..."
- $(MAKE) -C $(BUILDDIR)/latex all-pdf
- @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-.PHONY: text
-text:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder text $(ALLSPHINXOPTS) $(BUILDDIR)/text
- @echo
- @echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-.PHONY: man
-man:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder man $(ALLSPHINXOPTS) $(BUILDDIR)/man
- @echo
- @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-.PHONY: texinfo
-texinfo:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
- @echo
- @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
- @echo "Run \`make' in that directory to run these through makeinfo" \
- "(use \`make info' here to do that automatically)."
-
-.PHONY: info
-info:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
- @echo "Running Texinfo files through makeinfo..."
- make -C $(BUILDDIR)/texinfo info
- @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-.PHONY: gettext
-gettext:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
- @echo
- @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-.PHONY: changes
-changes:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
- @echo
- @echo "The overview file is in $(BUILDDIR)/changes."
-
.PHONY: linkcheck
linkcheck:
$(MAKE) install-sphinx
@@ -190,20 +62,14 @@ linkcheck:
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
-.PHONY: doctest
-doctest:
- $(MAKE) install-sphinx
- $(SPHINXBUILD) --builder doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
- @echo "Testing of doctests in the sources finished, look at the " \
- "results in $(BUILDDIR)/doctest/output.txt."
-
.PHONY: htmlview
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:
diff --git a/docs/PIL.rst b/docs/PIL.rst
index bdbf1373d..5225e9644 100644
--- a/docs/PIL.rst
+++ b/docs/PIL.rst
@@ -1,10 +1,10 @@
-PIL Package (autodoc of remaining modules)
+PIL package (autodoc of remaining modules)
==========================================
Reference for modules whose documentation has not yet been ported or written
can be found here.
-:mod:`PIL` Module
+:mod:`PIL` module
-----------------
.. py:module:: PIL
@@ -12,7 +12,7 @@ can be found here.
.. autoexception:: UnidentifiedImageError
:show-inheritance:
-:mod:`~PIL.BdfFontFile` Module
+:mod:`~PIL.BdfFontFile` module
------------------------------
.. automodule:: PIL.BdfFontFile
@@ -20,7 +20,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.ContainerIO` Module
+:mod:`~PIL.ContainerIO` module
------------------------------
.. automodule:: PIL.ContainerIO
@@ -28,7 +28,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.FontFile` Module
+:mod:`~PIL.FontFile` module
---------------------------
.. automodule:: PIL.FontFile
@@ -36,7 +36,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.GdImageFile` Module
+:mod:`~PIL.GdImageFile` module
------------------------------
.. automodule:: PIL.GdImageFile
@@ -44,7 +44,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.GimpGradientFile` Module
+:mod:`~PIL.GimpGradientFile` module
-----------------------------------
.. automodule:: PIL.GimpGradientFile
@@ -52,7 +52,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.GimpPaletteFile` Module
+:mod:`~PIL.GimpPaletteFile` module
----------------------------------
.. automodule:: PIL.GimpPaletteFile
@@ -60,7 +60,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.ImageDraw2` Module
+:mod:`~PIL.ImageDraw2` module
-----------------------------
.. automodule:: PIL.ImageDraw2
@@ -69,7 +69,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.ImageMode` Module
+:mod:`~PIL.ImageMode` module
----------------------------
.. automodule:: PIL.ImageMode
@@ -77,7 +77,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.PaletteFile` Module
+:mod:`~PIL.PaletteFile` module
------------------------------
.. automodule:: PIL.PaletteFile
@@ -85,7 +85,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.PcfFontFile` Module
+:mod:`~PIL.PcfFontFile` module
------------------------------
.. automodule:: PIL.PcfFontFile
@@ -93,7 +93,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:class:`.PngImagePlugin.iTXt` Class
+:class:`.PngImagePlugin.iTXt` class
-----------------------------------
.. autoclass:: PIL.PngImagePlugin.iTXt
@@ -107,7 +107,7 @@ can be found here.
:param lang: language code
:param tkey: UTF-8 version of the key name
-:class:`.PngImagePlugin.PngInfo` Class
+:class:`.PngImagePlugin.PngInfo` class
--------------------------------------
.. autoclass:: PIL.PngImagePlugin.PngInfo
@@ -116,7 +116,7 @@ can be found here.
:show-inheritance:
-:mod:`~PIL.TarIO` Module
+:mod:`~PIL.TarIO` module
------------------------
.. automodule:: PIL.TarIO
@@ -124,7 +124,7 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.WalImageFile` Module
+:mod:`~PIL.WalImageFile` module
-------------------------------
.. automodule:: PIL.WalImageFile
diff --git a/docs/conf.py b/docs/conf.py
index bfbcf9151..040301433 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -229,97 +229,6 @@ html_js_files = [
# implements a search results scorer. If empty, the default will be used.
# html_search_scorer = 'scorer.js'
-# Output file base name for HTML help builder.
-htmlhelp_basename = "PillowPILForkdoc"
-
-# -- Options for LaTeX output ---------------------------------------------
-
-latex_elements: dict[str, str] = {
- # The paper size ('letterpaper' or 'a4paper').
- # 'papersize': 'letterpaper',
- # The font size ('10pt', '11pt' or '12pt').
- # 'pointsize': '10pt',
- # Additional stuff for the LaTeX preamble.
- # 'preamble': '',
- # Latex figure (float) alignment
- # 'figure_align': 'htbp',
-}
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title,
-# author, documentclass [howto, manual, or own class]).
-latex_documents = [
- (
- master_doc,
- "PillowPILFork.tex",
- "Pillow (PIL Fork) Documentation",
- "Jeffrey A. Clark",
- "manual",
- )
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-# latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-# latex_use_parts = False
-
-# If true, show page references after internal links.
-# latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-# latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-# latex_appendices = []
-
-# If false, no module index is generated.
-# latex_domain_indices = True
-
-
-# -- Options for manual page output ---------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
- (master_doc, "pillowpilfork", "Pillow (PIL Fork) Documentation", [author], 1)
-]
-
-# If true, show URL addresses after external links.
-# man_show_urls = False
-
-
-# -- Options for Texinfo output -------------------------------------------
-
-# Grouping the document tree into Texinfo files. List of tuples
-# (source start file, target name, title, author,
-# dir menu entry, description, category)
-texinfo_documents = [
- (
- master_doc,
- "PillowPILFork",
- "Pillow (PIL Fork) Documentation",
- author,
- "PillowPILFork",
- "Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors.",
- "Miscellaneous",
- )
-]
-
-# Documents to append as an appendix to all manuals.
-# texinfo_appendices = []
-
-# If false, no module index is generated.
-# texinfo_domain_indices = True
-
-# How to display URL addresses: 'footnote', 'no', or 'inline'.
-# texinfo_show_urls = 'footnote'
-
-# If true, do not generate a @detailmenu in the "Top" node's menu.
-# texinfo_no_detailmenu = False
-
linkcheck_allowed_redirects = {
r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*",
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 634cee689..0490ba439 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -155,7 +155,7 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc
The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They
have been deprecated, and will be removed in Pillow 12 (2025-10-15).
-Specific WebP Feature Checks
+Specific WebP feature checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
@@ -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
diff --git a/docs/handbook/appendices.rst b/docs/handbook/appendices.rst
index 347a8848b..c20d8bc8b 100644
--- a/docs/handbook/appendices.rst
+++ b/docs/handbook/appendices.rst
@@ -8,4 +8,5 @@ Appendices
image-file-formats
text-anchors
+ third-party-plugins
writing-your-own-image-plugin
diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst
index 7da1078c1..c9d3f5e91 100644
--- a/docs/handbook/concepts.rst
+++ b/docs/handbook/concepts.rst
@@ -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)``
@@ -84,7 +84,7 @@ pixels.
.. _coordinate-system:
-Coordinate System
+Coordinate system
-----------------
The Python Imaging Library uses a Cartesian pixel coordinate system, with (0,0)
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 991cadaa2..5ca549c37 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -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
^^^
@@ -68,7 +145,7 @@ by DirectX.
DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
.. versionadded:: 3.4.0
- DXT3 images can be read in ``RGB`` mode and DX10 images can be read in
+ DXT3 images can be read in ``RGBA`` mode and DX10 images can be read in
``RGB`` and ``RGBA`` mode.
.. versionadded:: 6.0.0
@@ -93,6 +170,12 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
in ``P`` mode.
+.. versionadded:: 11.2.1
+ DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved::
+
+ im.save(out, pixel_format="DXT1")
+
+
DIB
^^^
@@ -229,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.
@@ -717,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
@@ -928,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.
@@ -1137,7 +1222,7 @@ numbers are returned as a tuple of ``(numerator, denominator)``.
.. deprecated:: 3.0.0
-Reading Multi-frame TIFF Images
+Reading multi-frame TIFF images
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and
@@ -1157,15 +1242,14 @@ 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
**append_images**
A list of images to append as additional frames. Each of the
- images in the list can be single or multiframe images. Note however, that for
- correct results, all the appended images should have the same
- ``encoderinfo`` and ``encoderconfig`` properties.
+ images in the list can be single or multiframe images.
.. versionadded:: 4.2.0
@@ -1309,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
@@ -1580,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
------------------
@@ -1612,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
@@ -1686,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
---------------------
diff --git a/docs/handbook/overview.rst b/docs/handbook/overview.rst
index 17964d1c5..ab22b9807 100644
--- a/docs/handbook/overview.rst
+++ b/docs/handbook/overview.rst
@@ -13,7 +13,7 @@ processing tool.
Let’s look at a few possible uses of this library.
-Image Archives
+Image archives
--------------
The Python Imaging Library is ideal for image archival and batch processing
@@ -24,7 +24,7 @@ The current version identifies and reads a large number of formats. Write
support is intentionally restricted to the most commonly used interchange and
presentation formats.
-Image Display
+Image display
-------------
The current release includes Tk :py:class:`~PIL.ImageTk.PhotoImage` and
@@ -36,7 +36,7 @@ support.
For debugging, there’s also a :py:meth:`~PIL.Image.Image.show` method which saves an image to
disk, and calls an external display utility.
-Image Processing
+Image processing
----------------
The library contains basic image processing functionality, including point operations, filtering with a set of built-in convolution kernels, and colour space conversions.
diff --git a/docs/handbook/third-party-plugins.rst b/docs/handbook/third-party-plugins.rst
new file mode 100644
index 000000000..a189a5773
--- /dev/null
+++ b/docs/handbook/third-party-plugins.rst
@@ -0,0 +1,18 @@
+Third-party plugins
+===================
+
+Pillow uses a plugin model which allows users to add their own
+decoders and encoders to the library, without any changes to the library
+itself.
+
+Here is a list of PyPI projects that offer additional plugins:
+
+* :pypi:`DjvuRleImagePlugin`: Plugin for the DjVu RLE image format as defined in the DjVuLibre docs.
+* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library.
+* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL.
+* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images.
+* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implemetation. Python bindings implemented using pybind11.
+* :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings.
+* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format.
+* :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text.
+* :pypi:`raw-pillow-opener`: Simple camera raw opener, based on the rawpy library.
diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst
index f771ae7ae..28c0abe44 100644
--- a/docs/handbook/tutorial.rst
+++ b/docs/handbook/tutorial.rst
@@ -122,7 +122,7 @@ This means that opening an image file is a fast operation, which is independent
of the file size and compression type. Here’s a simple script to quickly
identify a set of image files:
-Identify Image Files
+Identify image files
^^^^^^^^^^^^^^^^^^^^
::
@@ -399,7 +399,7 @@ Applying filters
.. image:: enhanced_hopper.webp
:align: center
-Point Operations
+Point operations
^^^^^^^^^^^^^^^^
The :py:meth:`~PIL.Image.Image.point` method can be used to translate the pixel
@@ -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
diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst
index 9e7d14c57..21a9124d7 100644
--- a/docs/handbook/writing-your-own-image-plugin.rst
+++ b/docs/handbook/writing-your-own-image-plugin.rst
@@ -1,6 +1,6 @@
.. _image-plugins:
-Writing Your Own Image Plugin
+Writing your own image plugin
=============================
Pillow uses a plugin model which allows you to add your own
@@ -329,7 +329,7 @@ The fields are used as follows:
.. _file-codecs:
-Writing Your Own File Codec in C
+Writing your own file codec in C
================================
There are 3 stages in a file codec's lifetime:
@@ -414,7 +414,7 @@ memory and release any resources from external libraries.
.. _file-codecs-py:
-Writing Your Own File Codec in Python
+Writing your own file codec in Python
=====================================
Python file decoders and encoders should derive from
diff --git a/docs/installation.rst b/docs/installation.rst
index b4bf2fa00..03f18c195 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -3,27 +3,27 @@
Installation
============
-Basic Installation
+Basic installation
------------------
.. Note:: This section has moved to :ref:`basic-installation`. Please update references accordingly.
-Python Support
+Python support
--------------
.. Note:: This section has moved to :ref:`python-support`. Please update references accordingly.
-Platform Support
+Platform support
----------------
.. Note:: This section has moved to :ref:`platform-support`. Please update references accordingly.
-Building From Source
+Building from source
--------------------
.. Note:: This section has moved to :ref:`building-from-source`. Please update references accordingly.
-Old Versions
+Old versions
------------
.. Note:: This section has moved to :ref:`old-versions`. Please update references accordingly.
diff --git a/docs/installation/basic-installation.rst b/docs/installation/basic-installation.rst
index 989f72ddd..f66ee8707 100644
--- a/docs/installation/basic-installation.rst
+++ b/docs/installation/basic-installation.rst
@@ -8,7 +8,7 @@
.. _basic-installation:
-Basic Installation
+Basic installation
==================
.. note::
diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst
index b400a3436..c72568b20 100644
--- a/docs/installation/building-from-source.rst
+++ b/docs/installation/building-from-source.rst
@@ -8,12 +8,12 @@
.. _building-from-source:
-Building From Source
+Building from source
====================
.. _external-libraries:
-External Libraries
+External libraries
------------------
.. note::
@@ -44,7 +44,7 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality
- * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
+ * Pillow has been tested with libtiff versions **3.x** and **4.0-4.7.0**
* **libfreetype** provides type related services
@@ -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
`_. 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.
@@ -248,7 +271,7 @@ After navigating to the Pillow directory, run::
.. _compressed archive from PyPI: https://pypi.org/project/pillow/#files
-Build Options
+Build options
^^^^^^^^^^^^^
* Config setting: ``-C parallel=n``. Can also be given
@@ -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.
@@ -294,7 +319,7 @@ Sample usage::
.. _old-versions:
-Old Versions
+Old versions
============
You can download old distributions from the `release history at PyPI
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index 9eafad3c4..93486d034 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -1,6 +1,6 @@
.. _platform-support:
-Platform Support
+Platform support
================
Current platform support for Pillow. Binary distributions are
@@ -9,7 +9,7 @@ should compile and run everywhere platform support is listed. In
general, we aim to support all current versions of Linux, macOS, and
Windows.
-Continuous Integration Targets
+Continuous integration targets
------------------------------
These platforms are built and tested for every change.
@@ -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 |
@@ -59,7 +59,7 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
-Other Platforms
+Other platforms
---------------
These platforms have been reported to work at the versions mentioned.
@@ -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 | |
+----------------------------------+----------------------------+------------------+--------------+
diff --git a/docs/installation/python-support.rst b/docs/installation/python-support.rst
index dd5765b6b..7daee8afc 100644
--- a/docs/installation/python-support.rst
+++ b/docs/installation/python-support.rst
@@ -1,6 +1,6 @@
.. _python-support:
-Python Support
+Python support
==============
Pillow supports these Python versions.
diff --git a/docs/make.bat b/docs/make.bat
index 0ed5ee1a5..4126f786b 100644
--- a/docs/make.bat
+++ b/docs/make.bat
@@ -22,20 +22,7 @@ if "%1" == "help" (
echo. htmlview to open the index page built by the html target in your browser
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
- echo. json to make JSON files
- echo. htmlhelp to make HTML files and a HTML help project
- echo. qthelp to make HTML files and a qthelp project
- echo. devhelp to make HTML files and a Devhelp project
- echo. epub to make an epub
- echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
- echo. text to make text files
- echo. man to make manual pages
- echo. texinfo to make Texinfo files
- echo. gettext to make PO message catalogs
- echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
- echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
@@ -80,107 +67,6 @@ if "%1" == "singlehtml" (
goto end
)
-if "%1" == "pickle" (
- %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can process the pickle files.
- goto end
-)
-
-if "%1" == "json" (
- %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can process the JSON files.
- goto end
-)
-
-if "%1" == "htmlhelp" (
- %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can run HTML Help Workshop with the ^
-.hhp project file in %BUILDDIR%/htmlhelp.
- goto end
-)
-
-if "%1" == "qthelp" (
- %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can run "qcollectiongenerator" with the ^
-.qhcp project file in %BUILDDIR%/qthelp, like this:
- echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PillowPILfork.qhcp
- echo.To view the help file:
- echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PillowPILfork.ghc
- goto end
-)
-
-if "%1" == "devhelp" (
- %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished.
- goto end
-)
-
-if "%1" == "epub" (
- %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The epub file is in %BUILDDIR%/epub.
- goto end
-)
-
-if "%1" == "latex" (
- %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
- goto end
-)
-
-if "%1" == "text" (
- %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The text files are in %BUILDDIR%/text.
- goto end
-)
-
-if "%1" == "man" (
- %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The manual pages are in %BUILDDIR%/man.
- goto end
-)
-
-if "%1" == "texinfo" (
- %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
- goto end
-)
-
-if "%1" == "gettext" (
- %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
- goto end
-)
-
-if "%1" == "changes" (
- %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
- if errorlevel 1 exit /b 1
- echo.
- echo.The overview file is in %BUILDDIR%/changes.
- goto end
-)
-
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
@@ -190,13 +76,4 @@ or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
-if "%1" == "doctest" (
- %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
- if errorlevel 1 exit /b 1
- echo.
- echo.Testing of doctests in the sources finished, look at the ^
-results in %BUILDDIR%/doctest/output.txt.
- goto end
-)
-
:end
diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst
index 06965ead3..e6bcd9d59 100644
--- a/docs/reference/ExifTags.rst
+++ b/docs/reference/ExifTags.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ExifTags
.. py:currentmodule:: PIL.ExifTags
-:py:mod:`~PIL.ExifTags` Module
+:py:mod:`~PIL.ExifTags` module
==============================
The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum`
diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst
index bc3758218..e68722900 100644
--- a/docs/reference/Image.rst
+++ b/docs/reference/Image.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.Image
.. py:currentmodule:: PIL.Image
-:py:mod:`~PIL.Image` Module
+:py:mod:`~PIL.Image` module
===========================
The :py:mod:`~PIL.Image` module provides a class with the same name which is
@@ -79,6 +79,7 @@ Constructing images
.. autofunction:: new
.. autofunction:: fromarray
+.. autofunction:: fromarrow
.. autofunction:: frombytes
.. autofunction:: frombuffer
@@ -112,7 +113,7 @@ Registering plugins
.. autofunction:: register_decoder
.. autofunction:: register_encoder
-The Image Class
+The Image class
---------------
.. autoclass:: PIL.Image.Image
@@ -260,7 +261,7 @@ method. ::
.. automethod:: PIL.Image.Image.load
.. automethod:: PIL.Image.Image.close
-Image Attributes
+Image attributes
----------------
Instances of the :py:class:`Image` class have the following attributes:
@@ -370,6 +371,8 @@ Protocols
.. autoclass:: SupportsArrayInterface
:show-inheritance:
+.. autoclass:: SupportsArrowArrayInterface
+ :show-inheritance:
.. autoclass:: SupportsGetData
:show-inheritance:
diff --git a/docs/reference/ImageChops.rst b/docs/reference/ImageChops.rst
index 9519361a7..505181db6 100644
--- a/docs/reference/ImageChops.rst
+++ b/docs/reference/ImageChops.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageChops
.. py:currentmodule:: PIL.ImageChops
-:py:mod:`~PIL.ImageChops` ("Channel Operations") Module
+:py:mod:`~PIL.ImageChops` ("channel operations") module
=======================================================
The :py:mod:`~PIL.ImageChops` module contains a number of arithmetical image
diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst
index 96bd14dd3..238390e75 100644
--- a/docs/reference/ImageCms.rst
+++ b/docs/reference/ImageCms.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageCms
.. py:currentmodule:: PIL.ImageCms
-:py:mod:`~PIL.ImageCms` Module
+:py:mod:`~PIL.ImageCms` module
==============================
The :py:mod:`~PIL.ImageCms` module provides color profile management
@@ -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
diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst
index 31faeac78..68e228dba 100644
--- a/docs/reference/ImageColor.rst
+++ b/docs/reference/ImageColor.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageColor
.. py:currentmodule:: PIL.ImageColor
-:py:mod:`~PIL.ImageColor` Module
+:py:mod:`~PIL.ImageColor` module
================================
The :py:mod:`~PIL.ImageColor` module contains color tables and converters from
@@ -11,7 +11,7 @@ others.
.. _color-names:
-Color Names
+Color names
-----------
The ImageColor module supports the following string formats:
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index b2f1bdc93..6e73233a1 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageDraw
.. py:currentmodule:: PIL.ImageDraw
-:py:mod:`~PIL.ImageDraw` Module
+:py:mod:`~PIL.ImageDraw` module
===============================
The :py:mod:`~PIL.ImageDraw` module provides simple 2D graphics for
@@ -54,7 +54,7 @@ later, you can also use RGB 3-tuples or color names (see below). The drawing
layer will automatically assign color indexes, as long as you don’t draw with
more than 256 colors.
-Color Names
+Color names
^^^^^^^^^^^
See :ref:`color-names` for the color names supported by Pillow.
@@ -75,7 +75,7 @@ To load a OpenType/TrueType font, use the truetype function in the
:py:mod:`~PIL.ImageFont` module. Note that this function depends on third-party
libraries, and may not available in all PIL builds.
-Example: Draw Partial Opacity Text
+Example: Draw partial opacity text
----------------------------------
::
@@ -102,7 +102,7 @@ Example: Draw Partial Opacity Text
out.show()
-Example: Draw Multiline Text
+Example: Draw multiline text
----------------------------
::
@@ -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.
diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst
index 529acad4a..334d1d4b2 100644
--- a/docs/reference/ImageEnhance.rst
+++ b/docs/reference/ImageEnhance.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageEnhance
.. py:currentmodule:: PIL.ImageEnhance
-:py:mod:`~PIL.ImageEnhance` Module
+:py:mod:`~PIL.ImageEnhance` module
==================================
The :py:mod:`~PIL.ImageEnhance` module contains a number of classes that can be used
diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst
index 64abd71d1..043559352 100644
--- a/docs/reference/ImageFile.rst
+++ b/docs/reference/ImageFile.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageFile
.. py:currentmodule:: PIL.ImageFile
-:py:mod:`~PIL.ImageFile` Module
+:py:mod:`~PIL.ImageFile` module
===============================
The :py:mod:`~PIL.ImageFile` module provides support functions for the image open
diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst
index 5f2b6af7c..1c201cacc 100644
--- a/docs/reference/ImageFilter.rst
+++ b/docs/reference/ImageFilter.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageFilter
.. py:currentmodule:: PIL.ImageFilter
-:py:mod:`~PIL.ImageFilter` Module
+:py:mod:`~PIL.ImageFilter` module
=================================
The :py:mod:`~PIL.ImageFilter` module contains definitions for a pre-defined set of
diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst
index d9d9cac6e..8b2f92323 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageFont
.. py:currentmodule:: PIL.ImageFont
-:py:mod:`~PIL.ImageFont` Module
+:py:mod:`~PIL.ImageFont` module
===============================
The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instances of
diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst
index db2987eb0..f6a2ec5bc 100644
--- a/docs/reference/ImageGrab.rst
+++ b/docs/reference/ImageGrab.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageGrab
.. py:currentmodule:: PIL.ImageGrab
-:py:mod:`~PIL.ImageGrab` Module
+:py:mod:`~PIL.ImageGrab` module
===============================
The :py:mod:`~PIL.ImageGrab` module can be used to copy the contents of the screen
@@ -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()
diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst
index f4e1081e6..0ee49b150 100644
--- a/docs/reference/ImageMath.rst
+++ b/docs/reference/ImageMath.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageMath
.. py:currentmodule:: PIL.ImageMath
-:py:mod:`~PIL.ImageMath` Module
+:py:mod:`~PIL.ImageMath` module
===============================
The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that
@@ -86,7 +86,7 @@ Expression syntax
It is not recommended to process expressions without considering this.
:py:meth:`lambda_eval` is a more secure alternative.
-Standard Operators
+Standard operators
^^^^^^^^^^^^^^^^^^
You can use standard arithmetical operators for addition (+), subtraction (-),
@@ -102,7 +102,7 @@ an 8-bit image, the result will be a 32-bit floating point image.
You can force conversion using the ``convert()``, ``float()``, and ``int()``
functions described below.
-Bitwise Operators
+Bitwise operators
^^^^^^^^^^^^^^^^^
The module also provides operations that operate on individual bits. This
@@ -116,7 +116,7 @@ mask off unwanted bits.
Bitwise operators don’t work on floating point images.
-Logical Operators
+Logical operators
^^^^^^^^^^^^^^^^^
Logical operators like ``and``, ``or``, and ``not`` work
@@ -128,7 +128,7 @@ treated as true.
Note that ``and`` and ``or`` return the last evaluated operand,
while not always returns a boolean value.
-Built-in Functions
+Built-in functions
^^^^^^^^^^^^^^^^^^
These functions are applied to each individual pixel.
diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst
index d4522a06a..30b89a54d 100644
--- a/docs/reference/ImageMorph.rst
+++ b/docs/reference/ImageMorph.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageMorph
.. py:currentmodule:: PIL.ImageMorph
-:py:mod:`~PIL.ImageMorph` Module
+:py:mod:`~PIL.ImageMorph` module
================================
The :py:mod:`~PIL.ImageMorph` module provides morphology operations on images.
diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst
index fcaa3c8f6..1ecff09f0 100644
--- a/docs/reference/ImageOps.rst
+++ b/docs/reference/ImageOps.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageOps
.. py:currentmodule:: PIL.ImageOps
-:py:mod:`~PIL.ImageOps` Module
+:py:mod:`~PIL.ImageOps` module
==============================
The :py:mod:`~PIL.ImageOps` module contains a number of ‘ready-made’ image
diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst
index 72ccfac7d..42ce5cb13 100644
--- a/docs/reference/ImagePalette.rst
+++ b/docs/reference/ImagePalette.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImagePalette
.. py:currentmodule:: PIL.ImagePalette
-:py:mod:`~PIL.ImagePalette` Module
+:py:mod:`~PIL.ImagePalette` module
==================================
The :py:mod:`~PIL.ImagePalette` module contains a class of the same name to
diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst
index 23544b613..5f5606349 100644
--- a/docs/reference/ImagePath.rst
+++ b/docs/reference/ImagePath.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImagePath
.. py:currentmodule:: PIL.ImagePath
-:py:mod:`~PIL.ImagePath` Module
+:py:mod:`~PIL.ImagePath` module
===============================
The :py:mod:`~PIL.ImagePath` module is used to store and manipulate 2-dimensional
diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst
index 7e67a44d3..88d7b8a20 100644
--- a/docs/reference/ImageQt.rst
+++ b/docs/reference/ImageQt.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageQt
.. py:currentmodule:: PIL.ImageQt
-:py:mod:`~PIL.ImageQt` Module
+:py:mod:`~PIL.ImageQt` module
=============================
The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6
diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst
index a27b2fb4e..0d6f394dd 100644
--- a/docs/reference/ImageSequence.rst
+++ b/docs/reference/ImageSequence.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageSequence
.. py:currentmodule:: PIL.ImageSequence
-:py:mod:`~PIL.ImageSequence` Module
+:py:mod:`~PIL.ImageSequence` module
===================================
The :py:mod:`~PIL.ImageSequence` module contains a wrapper class that lets you
diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst
index 5cedede69..12c8741ce 100644
--- a/docs/reference/ImageShow.rst
+++ b/docs/reference/ImageShow.rst
@@ -1,10 +1,10 @@
.. py:module:: PIL.ImageShow
.. py:currentmodule:: PIL.ImageShow
-:py:mod:`~PIL.ImageShow` Module
+:py:mod:`~PIL.ImageShow` module
===============================
-The :py:mod:`~PIL.ImageShow` Module is used to display images.
+The :py:mod:`~PIL.ImageShow` module is used to display images.
All default viewers convert the image to be shown to PNG format.
.. autofunction:: PIL.ImageShow.show
diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst
index f69466382..ede119920 100644
--- a/docs/reference/ImageStat.rst
+++ b/docs/reference/ImageStat.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageStat
.. py:currentmodule:: PIL.ImageStat
-:py:mod:`~PIL.ImageStat` Module
+:py:mod:`~PIL.ImageStat` module
===============================
The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or
diff --git a/docs/reference/ImageTk.rst b/docs/reference/ImageTk.rst
index 134ef5651..3ab72b83d 100644
--- a/docs/reference/ImageTk.rst
+++ b/docs/reference/ImageTk.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageTk
.. py:currentmodule:: PIL.ImageTk
-:py:mod:`~PIL.ImageTk` Module
+:py:mod:`~PIL.ImageTk` module
=============================
The :py:mod:`~PIL.ImageTk` module contains support to create and modify Tkinter
diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst
index 5b0a5ce49..530279934 100644
--- a/docs/reference/ImageTransform.rst
+++ b/docs/reference/ImageTransform.rst
@@ -2,7 +2,7 @@
.. py:module:: PIL.ImageTransform
.. py:currentmodule:: PIL.ImageTransform
-:py:mod:`~PIL.ImageTransform` Module
+:py:mod:`~PIL.ImageTransform` module
====================================
The :py:mod:`~PIL.ImageTransform` module contains implementations of
diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst
index 4151be4a7..c0b9bd2ba 100644
--- a/docs/reference/ImageWin.rst
+++ b/docs/reference/ImageWin.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.ImageWin
.. py:currentmodule:: PIL.ImageWin
-:py:mod:`~PIL.ImageWin` Module (Windows-only)
+:py:mod:`~PIL.ImageWin` module (Windows-only)
=============================================
The :py:mod:`~PIL.ImageWin` module contains support to create and display images on
diff --git a/docs/reference/JpegPresets.rst b/docs/reference/JpegPresets.rst
index aafae44cf..b0a3ba8b5 100644
--- a/docs/reference/JpegPresets.rst
+++ b/docs/reference/JpegPresets.rst
@@ -1,6 +1,6 @@
.. py:currentmodule:: PIL.JpegPresets
-:py:mod:`~PIL.JpegPresets` Module
+:py:mod:`~PIL.JpegPresets` module
=================================
.. automodule:: PIL.JpegPresets
diff --git a/docs/reference/PSDraw.rst b/docs/reference/PSDraw.rst
index 3e8512e7a..9eed775fc 100644
--- a/docs/reference/PSDraw.rst
+++ b/docs/reference/PSDraw.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.PSDraw
.. py:currentmodule:: PIL.PSDraw
-:py:mod:`~PIL.PSDraw` Module
+:py:mod:`~PIL.PSDraw` module
============================
The :py:mod:`~PIL.PSDraw` module provides simple print support for PostScript
diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst
index 1ac3d034b..9d7cf83b6 100644
--- a/docs/reference/PixelAccess.rst
+++ b/docs/reference/PixelAccess.rst
@@ -1,6 +1,6 @@
.. _PixelAccess:
-:py:class:`PixelAccess` Class
+:py:class:`PixelAccess` class
=============================
The PixelAccess class provides read and write access to
@@ -40,7 +40,7 @@ Access using negative indexes is also possible. ::
-:py:class:`PixelAccess` Class
+:py:class:`PixelAccess` class
-----------------------------
.. class:: PixelAccess
diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst
index 7cb7d16ae..d75a48478 100644
--- a/docs/reference/TiffTags.rst
+++ b/docs/reference/TiffTags.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.TiffTags
.. py:currentmodule:: PIL.TiffTags
-:py:mod:`~PIL.TiffTags` Module
+:py:mod:`~PIL.TiffTags` module
==============================
The :py:mod:`~PIL.TiffTags` module exposes many of the standard TIFF
diff --git a/docs/reference/arrow_support.rst b/docs/reference/arrow_support.rst
new file mode 100644
index 000000000..063046d8c
--- /dev/null
+++ b/docs/reference/arrow_support.rst
@@ -0,0 +1,88 @@
+.. _arrow-support:
+
+=============
+Arrow support
+=============
+
+`Arrow `__
+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.
diff --git a/docs/reference/block_allocator.rst b/docs/reference/block_allocator.rst
index 1abe5280f..5ad9d9fd1 100644
--- a/docs/reference/block_allocator.rst
+++ b/docs/reference/block_allocator.rst
@@ -1,7 +1,10 @@
-Block Allocator
+
+.. _block_allocator:
+
+Block allocator
===============
-Previous Design
+Previous design
---------------
Historically there have been two image allocators in Pillow:
@@ -13,7 +16,7 @@ large images and makes one allocation for each scan line of size
between one allocation and potentially thousands of small allocations,
leading to unpredictable performance penalties around the transition.
-New Design
+New design
----------
``ImagingAllocateArray`` now allocates space for images as a chain of
@@ -25,7 +28,7 @@ line. This is now the default for all internal allocations.
specifically requesting a single segment of memory for sharing with
other code.
-Memory Pools
+Memory pools
------------
There is now a memory pool to contain a supply of recently freed
@@ -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.
diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst
index 5e8586905..12dca6cf2 100644
--- a/docs/reference/c_extension_debugging.rst
+++ b/docs/reference/c_extension_debugging.rst
@@ -1,5 +1,5 @@
-C Extension debugging on Linux, with gbd/valgrind.
-==================================================
+C extension debugging on Linux, with GBD/Valgrind
+=================================================
Install the tools
-----------------
@@ -17,7 +17,7 @@ Then ``sudo apt-get install libtiff5-dbgsym``
- There's a bug with the ``python3-dbg`` package for at least Python 3.8 on
Ubuntu 20.04, and you need to add a new link or two to make it autoload when
- running python:
+ running Python:
::
@@ -49,7 +49,7 @@ Then ``sudo apt-get install libtiff5-dbgsym``
source ~/vpy38-dbg/bin/activate
cd ~/Pillow && make install
-Test Case
+Test case
---------
Take your test image, and make a really simple harness.
diff --git a/docs/reference/features.rst b/docs/reference/features.rst
index 0e173fe87..381d7830a 100644
--- a/docs/reference/features.rst
+++ b/docs/reference/features.rst
@@ -1,7 +1,7 @@
.. py:module:: PIL.features
.. py:currentmodule:: PIL.features
-:py:mod:`~PIL.features` Module
+:py:mod:`~PIL.features` module
==============================
The :py:mod:`PIL.features` module can be used to detect which Pillow features are available on your system.
@@ -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
diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst
index 99a18e9ea..6bba673b9 100644
--- a/docs/reference/internal_design.rst
+++ b/docs/reference/internal_design.rst
@@ -1,4 +1,4 @@
-Internal Reference
+Internal reference
==================
.. toctree::
@@ -9,3 +9,4 @@ Internal Reference
block_allocator
internal_modules
c_extension_debugging
+ arrow_support
diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst
index 93fd82cf9..19f78864d 100644
--- a/docs/reference/internal_modules.rst
+++ b/docs/reference/internal_modules.rst
@@ -1,7 +1,7 @@
-Internal Modules
+Internal modules
================
-:mod:`~PIL._binary` Module
+:mod:`~PIL._binary` module
--------------------------
.. automodule:: PIL._binary
@@ -9,7 +9,7 @@ Internal Modules
:undoc-members:
:show-inheritance:
-:mod:`~PIL._deprecate` Module
+:mod:`~PIL._deprecate` module
-----------------------------
.. automodule:: PIL._deprecate
@@ -17,7 +17,7 @@ Internal Modules
:undoc-members:
:show-inheritance:
-:mod:`~PIL._tkinter_finder` Module
+:mod:`~PIL._tkinter_finder` module
----------------------------------
.. automodule:: PIL._tkinter_finder
@@ -25,7 +25,7 @@ Internal Modules
:undoc-members:
:show-inheritance:
-:mod:`~PIL._typing` Module
+:mod:`~PIL._typing` module
--------------------------
.. module:: PIL._typing
@@ -58,7 +58,7 @@ on some Python versions.
See :py:obj:`typing.TypeGuard`.
-:mod:`~PIL._util` Module
+:mod:`~PIL._util` module
------------------------
.. automodule:: PIL._util
@@ -66,7 +66,7 @@ on some Python versions.
:undoc-members:
:show-inheritance:
-:mod:`~PIL._version` Module
+:mod:`~PIL._version` module
---------------------------
.. module:: PIL._version
@@ -78,7 +78,7 @@ on some Python versions.
This is the master version number for Pillow,
all other uses reference this module.
-:mod:`PIL.Image.core` Module
+:mod:`PIL.Image.core` module
----------------------------
.. module:: PIL._imaging
diff --git a/docs/reference/limits.rst b/docs/reference/limits.rst
index a71b514b5..d2f8f7d1f 100644
--- a/docs/reference/limits.rst
+++ b/docs/reference/limits.rst
@@ -4,7 +4,7 @@ Limits
This page is documentation to the various fundamental size limits in
the Pillow implementation.
-Internal Limits
+Internal limits
===============
* Image sizes cannot be negative. These are checked both in
@@ -25,10 +25,10 @@ Internal Limits
is smaller than 2GB, as calculated by ``y*stride`` (so 2Gpx for 'L'
images, and .5Gpx for 'RGB'
-Format Size Limits
+Format size limits
==================
* ICO: Max size is 256x256
-* Webp: 16383x16383 (underlying library size limit:
+* WebP: 16383x16383 (underlying library size limit:
https://developers.google.com/speed/webp/docs/api)
diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst
index 730c8da5b..0d43cbc73 100644
--- a/docs/reference/open_files.rst
+++ b/docs/reference/open_files.rst
@@ -1,6 +1,6 @@
.. _file-handling:
-File Handling in Pillow
+File handling in Pillow
=======================
When opening a file as an image, Pillow requires a filename, ``os.PathLike``
@@ -36,7 +36,7 @@ have multiple frames.
Pillow cannot in general close and reopen a file, so any access to
that file needs to be prior to the close.
-Image Lifecycle
+Image lifecycle
---------------
* ``Image.open()`` Filenames and ``Path`` objects are opened as a file.
@@ -97,7 +97,7 @@ Complications
im6.load() # FAILS, closed file
-Proposed File Handling
+Proposed file handling
----------------------
* ``Image.Image.load()`` should close the image file, unless there are
diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst
index 454b94d8c..243d4f353 100644
--- a/docs/reference/plugins.rst
+++ b/docs/reference/plugins.rst
@@ -1,7 +1,15 @@
Plugin reference
================
-:mod:`~PIL.BmpImagePlugin` Module
+:mod:`~PIL.AvifImagePlugin` module
+----------------------------------
+
+.. automodule:: PIL.AvifImagePlugin
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+:mod:`~PIL.BmpImagePlugin` module
---------------------------------
.. automodule:: PIL.BmpImagePlugin
@@ -9,7 +17,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.BufrStubImagePlugin` Module
+:mod:`~PIL.BufrStubImagePlugin` module
--------------------------------------
.. automodule:: PIL.BufrStubImagePlugin
@@ -17,7 +25,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.CurImagePlugin` Module
+:mod:`~PIL.CurImagePlugin` module
---------------------------------
.. automodule:: PIL.CurImagePlugin
@@ -25,7 +33,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.DcxImagePlugin` Module
+:mod:`~PIL.DcxImagePlugin` module
---------------------------------
.. automodule:: PIL.DcxImagePlugin
@@ -33,7 +41,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.DdsImagePlugin` Module
+:mod:`~PIL.DdsImagePlugin` module
---------------------------------
.. automodule:: PIL.DdsImagePlugin
@@ -41,7 +49,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.EpsImagePlugin` Module
+:mod:`~PIL.EpsImagePlugin` module
---------------------------------
.. automodule:: PIL.EpsImagePlugin
@@ -49,15 +57,15 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.FitsImagePlugin` Module
---------------------------------------
+:mod:`~PIL.FitsImagePlugin` module
+----------------------------------
.. automodule:: PIL.FitsImagePlugin
:members:
:undoc-members:
:show-inheritance:
-:mod:`~PIL.FliImagePlugin` Module
+:mod:`~PIL.FliImagePlugin` module
---------------------------------
.. automodule:: PIL.FliImagePlugin
@@ -65,7 +73,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.FpxImagePlugin` Module
+:mod:`~PIL.FpxImagePlugin` module
---------------------------------
.. automodule:: PIL.FpxImagePlugin
@@ -73,7 +81,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.GbrImagePlugin` Module
+:mod:`~PIL.GbrImagePlugin` module
---------------------------------
.. automodule:: PIL.GbrImagePlugin
@@ -81,7 +89,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.GifImagePlugin` Module
+:mod:`~PIL.GifImagePlugin` module
---------------------------------
.. automodule:: PIL.GifImagePlugin
@@ -89,7 +97,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.GribStubImagePlugin` Module
+:mod:`~PIL.GribStubImagePlugin` module
--------------------------------------
.. automodule:: PIL.GribStubImagePlugin
@@ -97,7 +105,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.Hdf5StubImagePlugin` Module
+:mod:`~PIL.Hdf5StubImagePlugin` module
--------------------------------------
.. automodule:: PIL.Hdf5StubImagePlugin
@@ -105,7 +113,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.IcnsImagePlugin` Module
+:mod:`~PIL.IcnsImagePlugin` module
----------------------------------
.. automodule:: PIL.IcnsImagePlugin
@@ -113,7 +121,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.IcoImagePlugin` Module
+:mod:`~PIL.IcoImagePlugin` module
---------------------------------
.. automodule:: PIL.IcoImagePlugin
@@ -121,7 +129,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.ImImagePlugin` Module
+:mod:`~PIL.ImImagePlugin` module
--------------------------------
.. automodule:: PIL.ImImagePlugin
@@ -129,7 +137,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.ImtImagePlugin` Module
+:mod:`~PIL.ImtImagePlugin` module
---------------------------------
.. automodule:: PIL.ImtImagePlugin
@@ -137,7 +145,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.IptcImagePlugin` Module
+:mod:`~PIL.IptcImagePlugin` module
----------------------------------
.. automodule:: PIL.IptcImagePlugin
@@ -145,7 +153,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.JpegImagePlugin` Module
+:mod:`~PIL.JpegImagePlugin` module
----------------------------------
.. automodule:: PIL.JpegImagePlugin
@@ -153,7 +161,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.Jpeg2KImagePlugin` Module
+:mod:`~PIL.Jpeg2KImagePlugin` module
------------------------------------
.. automodule:: PIL.Jpeg2KImagePlugin
@@ -161,7 +169,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.McIdasImagePlugin` Module
+:mod:`~PIL.McIdasImagePlugin` module
------------------------------------
.. automodule:: PIL.McIdasImagePlugin
@@ -169,7 +177,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.MicImagePlugin` Module
+:mod:`~PIL.MicImagePlugin` module
---------------------------------
.. automodule:: PIL.MicImagePlugin
@@ -177,7 +185,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.MpegImagePlugin` Module
+:mod:`~PIL.MpegImagePlugin` module
----------------------------------
.. automodule:: PIL.MpegImagePlugin
@@ -185,15 +193,15 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.MpoImagePlugin` Module
-----------------------------------
+:mod:`~PIL.MpoImagePlugin` module
+---------------------------------
.. automodule:: PIL.MpoImagePlugin
:members:
:undoc-members:
:show-inheritance:
-:mod:`~PIL.MspImagePlugin` Module
+:mod:`~PIL.MspImagePlugin` module
---------------------------------
.. automodule:: PIL.MspImagePlugin
@@ -201,7 +209,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.PalmImagePlugin` Module
+:mod:`~PIL.PalmImagePlugin` module
----------------------------------
.. automodule:: PIL.PalmImagePlugin
@@ -209,7 +217,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.PcdImagePlugin` Module
+:mod:`~PIL.PcdImagePlugin` module
---------------------------------
.. automodule:: PIL.PcdImagePlugin
@@ -217,7 +225,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.PcxImagePlugin` Module
+:mod:`~PIL.PcxImagePlugin` module
---------------------------------
.. automodule:: PIL.PcxImagePlugin
@@ -225,7 +233,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.PdfImagePlugin` Module
+:mod:`~PIL.PdfImagePlugin` module
---------------------------------
.. automodule:: PIL.PdfImagePlugin
@@ -233,7 +241,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.PixarImagePlugin` Module
+:mod:`~PIL.PixarImagePlugin` module
-----------------------------------
.. automodule:: PIL.PixarImagePlugin
@@ -241,7 +249,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.PngImagePlugin` Module
+:mod:`~PIL.PngImagePlugin` module
---------------------------------
.. automodule:: PIL.PngImagePlugin
@@ -252,7 +260,7 @@ Plugin reference
:member-order: groupwise
-:mod:`~PIL.PpmImagePlugin` Module
+:mod:`~PIL.PpmImagePlugin` module
---------------------------------
.. automodule:: PIL.PpmImagePlugin
@@ -260,7 +268,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.PsdImagePlugin` Module
+:mod:`~PIL.PsdImagePlugin` module
---------------------------------
.. automodule:: PIL.PsdImagePlugin
@@ -268,7 +276,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.SgiImagePlugin` Module
+:mod:`~PIL.SgiImagePlugin` module
---------------------------------
.. automodule:: PIL.SgiImagePlugin
@@ -276,7 +284,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.SpiderImagePlugin` Module
+:mod:`~PIL.SpiderImagePlugin` module
------------------------------------
.. automodule:: PIL.SpiderImagePlugin
@@ -284,7 +292,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.SunImagePlugin` Module
+:mod:`~PIL.SunImagePlugin` module
---------------------------------
.. automodule:: PIL.SunImagePlugin
@@ -292,7 +300,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.TgaImagePlugin` Module
+:mod:`~PIL.TgaImagePlugin` module
---------------------------------
.. automodule:: PIL.TgaImagePlugin
@@ -300,7 +308,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.TiffImagePlugin` Module
+:mod:`~PIL.TiffImagePlugin` module
----------------------------------
.. automodule:: PIL.TiffImagePlugin
@@ -308,7 +316,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.WebPImagePlugin` Module
+:mod:`~PIL.WebPImagePlugin` module
----------------------------------
.. automodule:: PIL.WebPImagePlugin
@@ -316,7 +324,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.WmfImagePlugin` Module
+:mod:`~PIL.WmfImagePlugin` module
---------------------------------
.. automodule:: PIL.WmfImagePlugin
@@ -324,7 +332,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.XVThumbImagePlugin` Module
+:mod:`~PIL.XVThumbImagePlugin` module
-------------------------------------
.. automodule:: PIL.XVThumbImagePlugin
@@ -332,7 +340,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.XbmImagePlugin` Module
+:mod:`~PIL.XbmImagePlugin` module
---------------------------------
.. automodule:: PIL.XbmImagePlugin
@@ -340,7 +348,7 @@ Plugin reference
:undoc-members:
:show-inheritance:
-:mod:`~PIL.XpmImagePlugin` Module
+:mod:`~PIL.XpmImagePlugin` module
---------------------------------
.. automodule:: PIL.XpmImagePlugin
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index 2ea973c5c..3e2aa84b1 100644
--- a/docs/releasenotes/10.0.0.rst
+++ b/docs/releasenotes/10.0.0.rst
@@ -28,7 +28,7 @@ This threshold can be changed by setting
:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting
``ImageFont.MAX_STRING_LENGTH = None``.
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
Categories
@@ -164,7 +164,7 @@ Since Pillow's C API is now faster than PyAccess on PyPy,
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
similarly deprecated.
-API Changes
+API changes
===========
Added line width parameter to ImageDraw regular_polygon
@@ -173,7 +173,7 @@ Added line width parameter to ImageDraw regular_polygon
An optional line ``width`` parameter has been added to
``ImageDraw.Draw.regular_polygon``.
-API Additions
+API additions
=============
Added ``alpha_only`` argument to ``getbbox()``
@@ -184,7 +184,7 @@ Added ``alpha_only`` argument to ``getbbox()``
and the image has an alpha channel, trim transparent pixels. Otherwise, trim
pixels when all channels are zero.
-Other Changes
+Other changes
=============
32-bit wheels
diff --git a/docs/releasenotes/10.0.1.rst b/docs/releasenotes/10.0.1.rst
index 02189d514..aa17a62e0 100644
--- a/docs/releasenotes/10.0.1.rst
+++ b/docs/releasenotes/10.0.1.rst
@@ -11,7 +11,7 @@ This release provides an updated install script and updated wheels to
include libwebp 1.3.2, preventing a potential heap buffer overflow in
WebP.
-Other Changes
+Other changes
=============
Updated tests to pass with latest zlib version
diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst
index fd556bdf1..996e6a3ed 100644
--- a/docs/releasenotes/10.1.0.rst
+++ b/docs/releasenotes/10.1.0.rst
@@ -1,7 +1,7 @@
10.1.0
------
-API Changes
+API changes
===========
Setting image mode
@@ -35,7 +35,7 @@ to be specified, rather than a single number for both dimensions. ::
ImageFilter.BoxBlur((2, 5))
ImageFilter.GaussianBlur((2, 5))
-API Additions
+API additions
=============
EpsImagePlugin.gs_binary
@@ -71,7 +71,7 @@ size and font_size arguments when using default font
Pillow has had a "better than nothing" default font, which can only be drawn at
one font size. Now, if FreeType support is available, a version of
-`Aileron Regular `_ is loaded, which can be
+`Aileron Regular `_ is loaded, which can be
drawn at chosen font sizes.
The following ``size`` and ``font_size`` arguments can now be used to specify a
@@ -84,7 +84,7 @@ font size for this new builtin font::
draw.multiline_text((0, 0), "test", font_size=24)
draw.multiline_textbbox((0, 0), "test", font_size=24)
-Other Changes
+Other changes
=============
Python 3.12
diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst
index 1c6b78b08..337748785 100644
--- a/docs/releasenotes/10.2.0.rst
+++ b/docs/releasenotes/10.2.0.rst
@@ -53,7 +53,7 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
for internal use, so there is no replacement. They can each be replaced
by a single line of code using builtin functions in Python.
-API Changes
+API changes
===========
Zero or negative font size error
@@ -63,7 +63,7 @@ When creating a :py:class:`~PIL.ImageFont.FreeTypeFont` instance, either directl
through :py:func:`~PIL.ImageFont.truetype`, if the font size is zero or less, a
:py:exc:`ValueError` will now be raised.
-API Additions
+API additions
=============
Added DdsImagePlugin enums
@@ -95,7 +95,7 @@ JPEG tables-only streamtype
When saving JPEG files, ``streamtype`` can now be set to 1, for tables-only. This will
output only the quantization and Huffman tables for the image.
-Other Changes
+Other changes
=============
Added DDS BC4U and DX10 BC1 and BC4 reading
diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst
index 2f0437d94..6c7d8ea0a 100644
--- a/docs/releasenotes/10.3.0.rst
+++ b/docs/releasenotes/10.3.0.rst
@@ -65,7 +65,7 @@ ImageMath.eval()
:py:meth:`~PIL.ImageMath.unsafe_eval` instead. See earlier security notes for more
information.
-API Changes
+API changes
===========
Added alpha_quality argument when saving WebP images
@@ -87,7 +87,7 @@ Negative P1-P3 PPM value error
If a P1-P3 PPM image contains a negative value, a :py:exc:`ValueError` will now be
raised.
-API Additions
+API additions
=============
Added PerspectiveTransform
@@ -97,7 +97,7 @@ Added PerspectiveTransform
that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding
subclass of :py:class:`~PIL.ImageTransform.Transform`.
-Other Changes
+Other changes
=============
Portable FloatMap (PFM) images
diff --git a/docs/releasenotes/10.4.0.rst b/docs/releasenotes/10.4.0.rst
index 8d3706be6..84a6091c9 100644
--- a/docs/releasenotes/10.4.0.rst
+++ b/docs/releasenotes/10.4.0.rst
@@ -41,7 +41,7 @@ ImageDraw.getdraw hints parameter
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
-API Additions
+API additions
=============
ImageDraw.circle
@@ -51,7 +51,7 @@ Added :py:meth:`~PIL.ImageDraw.ImageDraw.circle`. It provides the same functiona
:py:meth:`~PIL.ImageDraw.ImageDraw.ellipse`, but instead of taking a bounding box, it
takes a center point and radius.
-Other Changes
+Other changes
=============
Python 3.13 beta
diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst
index c3f18140f..020fbf7df 100644
--- a/docs/releasenotes/11.0.0.rst
+++ b/docs/releasenotes/11.0.0.rst
@@ -1,7 +1,7 @@
11.0.0
------
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
Python 3.8
@@ -103,7 +103,7 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc
The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They
have been deprecated, and will be removed in Pillow 12 (2025-10-15).
-Specific WebP Feature Checks
+Specific WebP feature checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
@@ -113,7 +113,7 @@ Specific WebP Feature Checks
``True`` if the WebP module is installed, until they are removed in Pillow
12.0.0 (2025-10-15).
-API Changes
+API changes
===========
Default resampling filter for I;16* image modes
@@ -122,7 +122,7 @@ Default resampling filter for I;16* image modes
The default resampling filter for I;16, I;16L, I;16B and I;16N has been changed from
``Image.NEAREST`` to ``Image.BICUBIC``, to match the majority of modes.
-API Additions
+API additions
=============
Writing XMP bytes to JPEG and MPO
@@ -138,7 +138,7 @@ either JPEG or MPO images::
im.info["xmp"] = b"test"
im.save("out.jpg")
-Other Changes
+Other changes
=============
Python 3.13
@@ -154,7 +154,7 @@ Support has also been added for the experimental free-threaded mode of :pep:`703
Python 3.13 only supports macOS versions 10.13 and later.
-C-level Flags
+C-level flags
^^^^^^^^^^^^^
Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other
diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst
index 0d56cb420..4888ddf56 100644
--- a/docs/releasenotes/11.1.0.rst
+++ b/docs/releasenotes/11.1.0.rst
@@ -10,7 +10,7 @@ ExifTags.IFD.Makernote
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
-API Changes
+API changes
===========
Writing XMP bytes to JPEG and MPO
@@ -34,7 +34,7 @@ be used::
second_im.encoderinfo = {"xmp": b"test"}
im.save("out.mpo", save_all=True, append_images=[second_im])
-API Additions
+API additions
=============
Check for zlib-ng
@@ -54,7 +54,7 @@ TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument::
im.save("out.tiff", big_tiff=True)
-Other Changes
+Other changes
=============
Reading JPEG 2000 comments
diff --git a/docs/releasenotes/11.2.0.rst b/docs/releasenotes/11.2.0.rst
deleted file mode 100644
index f7e644cf3..000000000
--- a/docs/releasenotes/11.2.0.rst
+++ /dev/null
@@ -1,75 +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
-
-Other Changes
-=============
-
-TODO
-^^^^
-
-TODO
diff --git a/docs/releasenotes/11.2.1.rst b/docs/releasenotes/11.2.1.rst
new file mode 100644
index 000000000..f55b0d7d7
--- /dev/null
+++ b/docs/releasenotes/11.2.1.rst
@@ -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 `__ 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.
diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst
index e9b0995bb..a1ddd1178 100644
--- a/docs/releasenotes/2.7.0.rst
+++ b/docs/releasenotes/2.7.0.rst
@@ -1,13 +1,13 @@
2.7.0
-----
-Sane Plugin
+Sane plugin
^^^^^^^^^^^
The Sane plugin has now been split into its own repo:
https://github.com/python-pillow/Sane .
-Png text chunk size limits
+PNG text chunk size limits
^^^^^^^^^^^^^^^^^^^^^^^^^^
To prevent potential denial of service attacks using compressed text
@@ -155,7 +155,7 @@ so the quality was worse compared to other Gaussian blur software.
The new implementation does not have this drawback.
-TIFF Parameter Changes
+TIFF parameter changes
^^^^^^^^^^^^^^^^^^^^^^
Several kwarg parameters for saving TIFF images were previously
diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst
index 8bc477f70..dcd8031f5 100644
--- a/docs/releasenotes/3.0.0.rst
+++ b/docs/releasenotes/3.0.0.rst
@@ -1,7 +1,7 @@
3.0.0
-----
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
Several methods that have been marked as deprecated for many releases
@@ -18,10 +18,10 @@ have been removed in this release:
* ``ImageWin.fromstring()``
* ``ImageWin.tostring()``
-Other Changes
+Other changes
=============
-Saving Multipage Images
+Saving multipage images
^^^^^^^^^^^^^^^^^^^^^^^
There is now support for saving multipage images in the ``GIF`` and
@@ -30,10 +30,10 @@ as a keyword argument to the save::
im.save('test.pdf', save_all=True)
-Tiff ImageFileDirectory Rewrite
+TIFF ImageFileDirectory rewrite
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-The Tiff ImageFileDirectory metadata code has been rewritten. Where
+The TIFF ImageFileDirectory metadata code has been rewritten. Where
previously it returned a somewhat arbitrary set of values and tuples,
it now returns bare values where appropriate and tuples when the
metadata item is a sequence or collection.
@@ -41,7 +41,7 @@ metadata item is a sequence or collection.
The original metadata is still available in the TiffImage.tags, the
new values are available in the TiffImage.tags_v2 member. The old
structures will be deprecated at some point in the future. When
-saving Tiff metadata, new code should use the
+saving TIFF metadata, new code should use the
TiffImagePlugin.ImageFileDirectory_v2 class.
LibJpeg and Zlib are required by default
diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst
index 951819f19..90f77ff61 100644
--- a/docs/releasenotes/3.1.0.rst
+++ b/docs/releasenotes/3.1.0.rst
@@ -22,7 +22,7 @@ not the absolute height of each line.
There is also now a default spacing of 4px between lines.
-Exif, Jpeg and Tiff Metadata
+EXIF, JPEG and TIFF metadata
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
There were major changes in the TIFF ImageFileDirectory support in
@@ -63,7 +63,7 @@ single item tuples have been unwrapped and return a bare element.
The format returned by Pillow 3.0 has been abandoned. A more fully
featured interface for EXIF is anticipated in a future release.
-Out of Spec Metadata
+Out of spec metadata
++++++++++++++++++++
In Pillow 3.0 and 3.1, images that contain metadata that is internally
diff --git a/docs/releasenotes/3.2.0.rst b/docs/releasenotes/3.2.0.rst
index 3ed8fae57..20d7d073e 100644
--- a/docs/releasenotes/3.2.0.rst
+++ b/docs/releasenotes/3.2.0.rst
@@ -1,7 +1,7 @@
3.2.0
-----
-New DDS and FTEX Image Plugins
+New DDS and FTEX image plugins
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``DdsImagePlugin`` reading DXT1 and DXT5 encoded ``.dds`` images was
@@ -18,7 +18,7 @@ Updates to the GbrImagePlugin
The ``GbrImagePlugin`` (GIMP brush format) has been updated to fix
support for version 1 files and add support for version 2 files.
-Passthrough Parameters for ImageDraw.text
+Passthrough parameters for ImageDraw.text
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``ImageDraw.multiline_text`` and ``ImageDraw.multiline_size`` take extra
diff --git a/docs/releasenotes/3.3.0.rst b/docs/releasenotes/3.3.0.rst
index cd6f7e2f9..9447245c4 100644
--- a/docs/releasenotes/3.3.0.rst
+++ b/docs/releasenotes/3.3.0.rst
@@ -11,7 +11,7 @@ libimagequant. We cannot distribute binaries due to licensing
differences.
-New Setup.py options
+New setup.py options
^^^^^^^^^^^^^^^^^^^^
There are two new options to control the ``build_ext`` task in ``setup.py``:
@@ -43,7 +43,7 @@ This greatly improves both quality and performance in this case.
Also, the bug with wrong image size calculation when rotating by 90 degrees
was fixed.
-Image Metadata
+Image metadata
^^^^^^^^^^^^^^
The return type for binary data in version 2 Exif and Tiff metadata
diff --git a/docs/releasenotes/3.3.2.rst b/docs/releasenotes/3.3.2.rst
index 73156a65d..60ffbdcba 100644
--- a/docs/releasenotes/3.3.2.rst
+++ b/docs/releasenotes/3.3.2.rst
@@ -4,7 +4,7 @@
Security
========
-Integer overflow in Map.c
+Integer overflow in map.c
^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow prior to 3.3.2 may experience integer overflow errors in map.c
@@ -27,7 +27,7 @@ memory without duplicating the image first.
This issue was found by Cris Neckar at Divergent Security.
-Sign Extension in Storage.c
+Sign extension in Storage.c
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow prior to 3.3.2 and PIL 1.1.7 (at least) do not check for
diff --git a/docs/releasenotes/3.4.0.rst b/docs/releasenotes/3.4.0.rst
index 8a5a7efe3..01ec77a58 100644
--- a/docs/releasenotes/3.4.0.rst
+++ b/docs/releasenotes/3.4.0.rst
@@ -1,7 +1,7 @@
3.4.0
-----
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
Image.core.open_ppm removed
@@ -14,7 +14,7 @@ been removed. If you were using this function, please use
Deprecations
============
-Deprecation Warning when Saving JPEGs
+Deprecation warning when saving JPEGs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0
@@ -22,7 +22,7 @@ silently drops the alpha channel. With this release Pillow will now
issue a :py:exc:`DeprecationWarning` when attempting to save a ``RGBA`` mode
image as a JPEG. This will become an error in Pillow 4.2.
-API Additions
+API additions
=============
New resizing filters
@@ -37,7 +37,7 @@ two times shorter window than ``BILINEAR``. It can be used for image reduction
providing the image downscaling quality comparable to ``BICUBIC``.
Both new filters don't show good quality for the image upscaling.
-New DDS Decoders
+New DDS decoders
^^^^^^^^^^^^^^^^
Pillow can now decode DXT3 images, as well as the previously supported
diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst
index 625f237e8..dd97463f6 100644
--- a/docs/releasenotes/4.0.0.rst
+++ b/docs/releasenotes/4.0.0.rst
@@ -1,7 +1,7 @@
4.0.0
-----
-Python 2.6 and 3.2 Dropped
+Python 2.6 and 3.2 dropped
^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow 4.0 no longer supports Python 2.6 and 3.2. We will not be
diff --git a/docs/releasenotes/4.1.0.rst b/docs/releasenotes/4.1.0.rst
index 80ad9b9fb..1f809ad18 100644
--- a/docs/releasenotes/4.1.0.rst
+++ b/docs/releasenotes/4.1.0.rst
@@ -15,10 +15,10 @@ Several deprecated items have been removed.
``PIL.ImageDraw.ImageDraw.setfont`` have been removed.
-Other Changes
+Other changes
=============
-Closing Files When Opening Images
+Closing files when opening images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The file handling when opening images has been overhauled. Previously,
@@ -41,7 +41,7 @@ is specified:
the underlying file until we are done with the image. The mapping
will be closed in the ``close`` or ``__del__`` method.
-Changes to GIF Handling When Saving
+Changes to GIF handling when saving
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The :py:class:`PIL.GifImagePlugin` code has been refactored to fix the flow when
@@ -57,14 +57,14 @@ saving images. There are two external changes that arise from this:
This refactor fixed some bugs with palette handling when saving
multiple frame GIFs.
-New Method: Image.remap_palette
+New method: Image.remap_palette
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The method :py:meth:`PIL.Image.Image.remap_palette()` has been
added. This method was hoisted from the GifImagePlugin code used to
optimize the palette.
-Added Decoder Registry and Support for Python Based Decoders
+Added decoder registry and support for Python-based decoders
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
There is now a decoder registry similar to the image plugin
diff --git a/docs/releasenotes/4.1.1.rst b/docs/releasenotes/4.1.1.rst
index 8c8055bfa..1cbd3853b 100644
--- a/docs/releasenotes/4.1.1.rst
+++ b/docs/releasenotes/4.1.1.rst
@@ -1,7 +1,7 @@
4.1.1
-----
-Fix Regression with reading DPI from EXIF data
+Fix regression with reading DPI from EXIF data
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Some JPEG images don't contain DPI information in the image metadata,
diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst
index bc2a45f02..0ea3de399 100644
--- a/docs/releasenotes/4.2.0.rst
+++ b/docs/releasenotes/4.2.0.rst
@@ -1,7 +1,7 @@
4.2.0
-----
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
Several deprecated items have been removed
@@ -17,17 +17,17 @@ Several deprecated items have been removed
was shown. From Pillow 4.2.0, the deprecation warning is removed and
an :py:exc:`IOError` is raised.
-Removed Core Image Function
+Removed core Image function
^^^^^^^^^^^^^^^^^^^^^^^^^^^
The unused function ``Image.core.new_array`` was removed. This is an
internal function that should not have been used by user code, but it
was accessible from the python layer.
-Other Changes
+Other changes
=============
-Added Complex Text Rendering
+Added complex text rendering
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow now supports complex text rendering for scripts requiring glyph
@@ -36,7 +36,7 @@ dependencies: harfbuzz, fribidi, and raqm. See the :doc:`install documentation
<../installation>` for further details. This feature is tested and works on
Unix and Mac, but has not yet been built on Windows platforms.
-New Optional Parameters
+New optional parameters
^^^^^^^^^^^^^^^^^^^^^^^
* :py:meth:`PIL.ImageDraw.floodfill` has a new optional parameter:
@@ -47,7 +47,7 @@ New Optional Parameters
optional parameter for specifying additional images to create
multipage outputs.
-New DecompressionBomb Warning
+New DecompressionBomb warning
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:meth:`PIL.Image.Image.crop` now may raise a DecompressionBomb
diff --git a/docs/releasenotes/4.2.1.rst b/docs/releasenotes/4.2.1.rst
index 2061f6467..617d51e52 100644
--- a/docs/releasenotes/4.2.1.rst
+++ b/docs/releasenotes/4.2.1.rst
@@ -3,7 +3,7 @@
There are no functional changes in this release.
-Fixed Windows PyPy Build
+Fixed Windows PyPy build
^^^^^^^^^^^^^^^^^^^^^^^^
A change in the 4.2.0 cycle broke the Windows PyPy build. This has
diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst
index ea81fc45e..87a57799f 100644
--- a/docs/releasenotes/4.3.0.rst
+++ b/docs/releasenotes/4.3.0.rst
@@ -1,7 +1,7 @@
4.3.0
-----
-API Changes
+API changes
===========
Deprecations
@@ -12,7 +12,7 @@ Several undocumented functions in ImageOps have been deprecated:
``box_blur``. Use the equivalent operations in ``ImageFilter``
instead. These functions will be removed in a future release.
-TIFF Metadata Changes
+TIFF metadata changes
^^^^^^^^^^^^^^^^^^^^^
* TIFF tags with unknown type/quantity now default to being bare
@@ -27,7 +27,7 @@ TIFF Metadata Changes
items, as there can be multiple items, one for UTF-8, and one for
UTF-16.
-Core Image API Changes
+Core Image API changes
^^^^^^^^^^^^^^^^^^^^^^
These are internal functions that should not have been used by user
@@ -44,10 +44,10 @@ The ``PIL.Image.core.getcount`` methods have been removed, use
``PIL.Image.core.get_stats()['new_count']`` property instead.
-API Additions
+API additions
=============
-Get One Channel From Image
+Get one channel from image
^^^^^^^^^^^^^^^^^^^^^^^^^^
A new method :py:meth:`PIL.Image.Image.getchannel` has been added to
@@ -56,14 +56,14 @@ return a single channel by index or name. For example,
``getchannel`` should work up to 6 times faster than
``image.split()[0]`` in previous Pillow versions.
-Box Blur
+Box blur
^^^^^^^^
A new filter, :py:class:`PIL.ImageFilter.BoxBlur`, has been
added. This is a filter with similar results to a Gaussian blur, but
is much faster.
-Partial Resampling
+Partial resampling
^^^^^^^^^^^^^^^^^^
Added new argument ``box`` for :py:meth:`PIL.Image.Image.resize`. This
@@ -71,14 +71,14 @@ argument defines a source rectangle from within the source image to be
resized. This is very similar to the ``image.crop(box).resize(size)``
sequence except that ``box`` can be specified with subpixel accuracy.
-New Transpose Operation
+New transpose operation
^^^^^^^^^^^^^^^^^^^^^^^
The ``Image.TRANSVERSE`` operation has been added to
:py:meth:`PIL.Image.Image.transpose`. This is equivalent to a transpose
operation about the opposite diagonal.
-Multiband Filters
+Multiband filters
^^^^^^^^^^^^^^^^^
There is a new :py:class:`PIL.ImageFilter.MultibandFilter` base class
@@ -87,10 +87,10 @@ operation. The original :py:class:`PIL.ImageFilter.Filter` class
remains for image filters that can process only single band images, or
require splitting of channels prior to filtering.
-Other Changes
+Other changes
=============
-Loading 16-bit TIFF Images
+Loading 16-bit TIFF images
^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow now can read 16-bit multichannel TIFF files including files
@@ -101,7 +101,7 @@ Pillow now can read 16-bit signed integer single channel TIFF
files. The image data is promoted to 32-bit for storage and
processing.
-SGI Images
+SGI images
^^^^^^^^^^
Pillow can now read and write uncompressed 16-bit multichannel SGI
@@ -129,7 +129,7 @@ This release contains several performance improvements:
falling back to an allocation for each scan line for images larger
than the block size.
-CMYK Conversion
+CMYK conversion
^^^^^^^^^^^^^^^
The basic CMYK->RGB conversion has been tweaked to match the formula
diff --git a/docs/releasenotes/5.0.0.rst b/docs/releasenotes/5.0.0.rst
index be00a45cd..2b93e0322 100644
--- a/docs/releasenotes/5.0.0.rst
+++ b/docs/releasenotes/5.0.0.rst
@@ -1,10 +1,10 @@
5.0.0
-----
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
-Python 3.3 Dropped
+Python 3.3 dropped
^^^^^^^^^^^^^^^^^^
Python 3.3 is EOL and no longer supported due to moving testing from nose,
@@ -12,7 +12,7 @@ which is deprecated, to pytest, which doesn't support Python 3.3. We will not
be creating binaries, testing, or retaining compatibility with this version.
The final version of Pillow for Python 3.3 is 4.3.0.
-Decompression Bombs now raise Exceptions
+Decompression bombs now raise exceptions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow has previously emitted warnings for images that are
@@ -31,7 +31,7 @@ separate package, pillow-scripts, living at
https://github.com/python-pillow/pillow-scripts.
-API Changes
+API changes
===========
OleFileIO.py
@@ -54,7 +54,7 @@ Several image plugins supported a named ``check`` parameter on their
nominally private ``_save`` method to preflight if the image could be
saved in that format. That parameter has been removed.
-API Additions
+API additions
=============
Image.transform
@@ -65,16 +65,16 @@ A new named parameter, ``fillcolor``, has been added to
the area outside the transformed area in the output image. This
parameter takes the same color specifications as used in ``Image.new``.
-GIF Disposal
+GIF disposal
^^^^^^^^^^^^
Multiframe GIF images now take an optional disposal parameter to
specify the disposal option for changed pixels.
-Other Changes
+Other changes
=============
-Compressed TIFF Images
+Compressed TIFF images
^^^^^^^^^^^^^^^^^^^^^^
Previously, there were some compression modes (JPEG, Packbits, and
@@ -82,7 +82,7 @@ LZW) that were supported with Pillow's internal TIFF decoder. All
compressed TIFFs are now read using the ``libtiff`` decoder, as it
implements the compression schemes more correctly.
-Libraqm is now Dynamically Linked
+Libraqm is now dynamically linked
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The libraqm dependency for complex text scripts is now linked
@@ -90,14 +90,14 @@ dynamically at runtime rather than at packaging time. This allows us
to release binaries with support for libraqm if it is installed on the
user's machine.
-Source Layout Changes
+Source layout changes
^^^^^^^^^^^^^^^^^^^^^
The Pillow source is now stored within the ``src`` directory of the
distribution. This prevents accidental imports of the PIL directory
when running Python from the project directory.
-Setup.py Changes
+Setup.py changes
^^^^^^^^^^^^^^^^
Multiarch support on Linux should be more robust, especially on Debian
diff --git a/docs/releasenotes/5.1.0.rst b/docs/releasenotes/5.1.0.rst
index 4e3d10ac5..4b80e8521 100644
--- a/docs/releasenotes/5.1.0.rst
+++ b/docs/releasenotes/5.1.0.rst
@@ -1,7 +1,7 @@
5.1.0
-----
-API Changes
+API changes
===========
Optional channels for TIFF files
@@ -12,22 +12,22 @@ and ``CMYK`` with up to 6 8-bit channels, discarding any extra
channels if the content is tagged as UNSPECIFIED. Pillow still does
not store more than 4 8-bit channels of image data.
-API Additions
+API additions
=============
-Append to PDF Files
+Append to PDF files
^^^^^^^^^^^^^^^^^^^
Images can now be appended to PDF files in place by passing in
``append=True`` when saving the image.
-New BLP File Format
+New BLP file format
^^^^^^^^^^^^^^^^^^^
Pillow now supports reading the BLP "Blizzard Mipmap" file format used
for tiles in Blizzard's engine.
-Other Changes
+Other changes
=============
WebP memory leak
diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst
index d9b8f0fb7..d18337820 100644
--- a/docs/releasenotes/5.2.0.rst
+++ b/docs/releasenotes/5.2.0.rst
@@ -1,7 +1,7 @@
5.2.0
-----
-API Changes
+API changes
===========
Deprecations
@@ -17,7 +17,7 @@ Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that.
Use ``PIL.__version__`` instead.
-API Additions
+API additions
=============
3D color lookup tables
@@ -75,7 +75,7 @@ TGA file format
Pillow can now read and write LA data (in addition to L, P, RGB and RGBA), and
write RLE data (in addition to uncompressed).
-Other Changes
+Other changes
=============
Support added for Python 3.7
diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst
index 8f276da24..6adce95b2 100644
--- a/docs/releasenotes/5.3.0.rst
+++ b/docs/releasenotes/5.3.0.rst
@@ -1,7 +1,7 @@
5.3.0
-----
-API Changes
+API changes
===========
Image size
@@ -20,7 +20,7 @@ The exceptions to this are:
as direct image size setting was previously necessary to work around an issue with tile extents.
-API Additions
+API additions
=============
Added line width parameter to rectangle and ellipse-based shapes
@@ -59,7 +59,7 @@ and size, new method ``ImageOps.pad`` pads images to fill a requested aspect
ratio and size, filling new space with a provided ``color`` and positioning the
image within the new area through a ``centering`` argument.
-Other Changes
+Other changes
=============
Added support for reading tiled TIFF images through LibTIFF. Compressed TIFF
diff --git a/docs/releasenotes/5.4.0.rst b/docs/releasenotes/5.4.0.rst
index 6d7277c70..13b540d60 100644
--- a/docs/releasenotes/5.4.0.rst
+++ b/docs/releasenotes/5.4.0.rst
@@ -1,7 +1,7 @@
5.4.0
-----
-API Changes
+API changes
===========
APNG extension to PNG plugin
@@ -55,7 +55,7 @@ TIFF images can now be saved with custom integer, float and string TIFF tags::
print(im2.tag_v2[37002]) # "custom tag value"
print(im2.tag_v2[37004]) # b"custom tag value"
-Other Changes
+Other changes
=============
ImageOps.fit
diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst
index 5e69f0b6b..b788b2eeb 100644
--- a/docs/releasenotes/6.0.0.rst
+++ b/docs/releasenotes/6.0.0.rst
@@ -1,7 +1,7 @@
6.0.0
-----
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
Python 3.4 dropped
@@ -32,7 +32,7 @@ Removed deprecated VERSION
``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__``
instead.
-API Changes
+API changes
===========
Deprecations
@@ -137,7 +137,7 @@ loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anym
The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and
"image/x-icon" respectively.
-API Additions
+API additions
=============
DIB file format
@@ -186,7 +186,7 @@ EXIF data can now be read from and saved to PNG images. However, unlike other im
formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info`
until :py:meth:`~PIL.Image.Image.load` has been called.
-Other Changes
+Other changes
=============
Reading new DDS image format
diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst
index ce3edc5fa..761f435f3 100644
--- a/docs/releasenotes/6.1.0.rst
+++ b/docs/releasenotes/6.1.0.rst
@@ -23,7 +23,7 @@ Use instead::
with Image.open("hopper.png") as im:
im.save("out.jpg")
-API Additions
+API additions
=============
Image.entropy
@@ -61,7 +61,7 @@ file. ``ImageFont.FreeTypeFont`` has four new methods,
instead. An :py:exc:`IOError` will be raised if the font is not a variation font. FreeType
2.9.1 or greater is required.
-Other Changes
+Other changes
=============
ImageTk.getimage
diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst
index b851c56fc..b37cd7160 100644
--- a/docs/releasenotes/6.2.0.rst
+++ b/docs/releasenotes/6.2.0.rst
@@ -29,7 +29,7 @@ perform operations on it.
The CVE is regarding DOS problems, such as consuming large amounts of memory,
or taking a large amount of time to process an image.
-API Changes
+API changes
===========
Image.getexif
@@ -48,7 +48,7 @@ There has been a longstanding warning that the defaults of ``Image.frombuffer``
may change in the future for the "raw" decoder. The change will now take place
in Pillow 7.0.
-API Additions
+API additions
=============
Text stroking
@@ -93,7 +93,7 @@ ImageGrab on multi-monitor Windows
An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``,
all monitors will be included in the created image.
-Other Changes
+Other changes
=============
Removed bdist_wininst .exe installers
diff --git a/docs/releasenotes/6.2.1.rst b/docs/releasenotes/6.2.1.rst
index 372298fbc..0ede05917 100644
--- a/docs/releasenotes/6.2.1.rst
+++ b/docs/releasenotes/6.2.1.rst
@@ -1,7 +1,7 @@
6.2.1
-----
-API Changes
+API changes
===========
Deprecations
@@ -15,7 +15,7 @@ Python 2.7 reaches end-of-life on 2020-01-01.
Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python
2.7, making Pillow 6.2.x the last release series to support Python 2.
-Other Changes
+Other changes
=============
Support added for Python 3.8
diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst
index ed6026593..9504c974a 100644
--- a/docs/releasenotes/7.0.0.rst
+++ b/docs/releasenotes/7.0.0.rst
@@ -1,7 +1,7 @@
7.0.0
-----
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
Python 2.7
@@ -78,7 +78,7 @@ bounds of resulting image. This may be useful in a subsequent
.. _chain methods: https://en.wikipedia.org/wiki/Method_chaining
-API Additions
+API additions
=============
Custom unidentified image error
@@ -124,7 +124,7 @@ now also be loaded at another resolution::
with Image.open("drawing.wmf") as im:
im.load(dpi=144)
-Other Changes
+Other changes
=============
Image.__del__
diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst
index 0dd8669a5..c2aeb0f74 100644
--- a/docs/releasenotes/7.1.0.rst
+++ b/docs/releasenotes/7.1.0.rst
@@ -35,7 +35,7 @@ out-of-bounds reads via a crafted JP2 file.
In ``libImaging/SgiRleDecode.c`` in Pillow through 7.0.0, a number of out-of-bounds
reads exist in the parsing of SGI image files, a different issue than :cve:`2020-5311`.
-API Changes
+API changes
===========
Allow saving of zero quality JPEG images
@@ -50,7 +50,7 @@ been resolved. ::
im = Image.open("hopper.jpg")
im.save("out.jpg", quality=0)
-API Additions
+API additions
=============
New channel operations
@@ -101,7 +101,7 @@ Passing a different value on Windows or macOS will force taking a snapshot
using the selected X server; pass an empty string to use the default X server.
XCB support is not included in pre-compiled wheels for Windows and macOS.
-Other Changes
+Other changes
=============
If present, only use alpha channel for bounding box
diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst
index 91e54da19..12bafa8ce 100644
--- a/docs/releasenotes/7.2.0.rst
+++ b/docs/releasenotes/7.2.0.rst
@@ -1,7 +1,7 @@
7.2.0
-----
-API Changes
+API changes
===========
Replaced TiffImagePlugin DEBUG with logging
diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst
index 1fc245c9a..d0dde756f 100644
--- a/docs/releasenotes/8.0.0.rst
+++ b/docs/releasenotes/8.0.0.rst
@@ -1,7 +1,7 @@
8.0.0
-----
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
Python 3.5
@@ -44,7 +44,7 @@ Removed Use instead
``product_model`` Unicode :py:attr:`~.CmsProfile.model`
======================== ===================================================
-API Changes
+API changes
===========
ImageDraw.text: stroke_width
@@ -67,7 +67,7 @@ Add MIME type to PsdImagePlugin
"image/vnd.adobe.photoshop" is now registered as the
:py:class:`.PsdImagePlugin.PsdImageFile` MIME type.
-API Additions
+API additions
=============
Image.open: add formats parameter
@@ -135,7 +135,7 @@ and :py:meth:`.FreeTypeFont.getbbox` return the bounding box of rendered text.
These functions accept an ``anchor`` parameter, see :ref:`text-anchors` for details.
-Other Changes
+Other changes
=============
Improved ellipse-drawing algorithm
diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst
index 5c3993318..06e6d9974 100644
--- a/docs/releasenotes/8.1.0.rst
+++ b/docs/releasenotes/8.1.0.rst
@@ -26,7 +26,7 @@ leading to an out-of-bounds write in ``TiffDecode.c``. This potentially affects
versions from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through
`Tidelift`_.
-:cve:`2020-35655`: SGI Decode buffer overrun
+:cve:`2020-35655`: SGI decode buffer overrun
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly
@@ -64,7 +64,7 @@ Makefile
The ``install-venv`` target has been deprecated.
-API Additions
+API additions
=============
Append images to ICO
@@ -77,7 +77,7 @@ With this release, a list of images can be provided to the ``append_images`` par
when saving, to replace the scaled down versions. This is the same functionality that
already exists for the ICNS format.
-Other Changes
+Other changes
=============
Makefile
diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst
index 690421c2a..b8ad5a898 100644
--- a/docs/releasenotes/8.1.1.rst
+++ b/docs/releasenotes/8.1.1.rst
@@ -32,7 +32,7 @@ DOS attack.
There is an out-of-bounds read in ``SgiRleDecode.c`` since Pillow 4.3.0.
-Other Changes
+Other changes
=============
A crash with the feature flags for libimagequant, libjpeg-turbo, WebP and XCB on
diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst
index 50fe9aa19..a59560695 100644
--- a/docs/releasenotes/8.2.0.rst
+++ b/docs/releasenotes/8.2.0.rst
@@ -74,7 +74,7 @@ Tk/Tcl 8.4
Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01),
when Tk/Tcl 8.5 will be the minimum supported.
-API Changes
+API changes
===========
Image.alpha_composite: dest
@@ -107,7 +107,7 @@ removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``,
``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()``
can be used.
-API Additions
+API additions
=============
getxmp() for JPEG images
@@ -177,7 +177,7 @@ be specified through a keyword argument::
im.save("out.tif", icc_profile=...)
-Other Changes
+Other changes
=============
GIF writer uses LZW encoding
diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst
index 4ef914f64..c46240854 100644
--- a/docs/releasenotes/8.3.0.rst
+++ b/docs/releasenotes/8.3.0.rst
@@ -33,7 +33,7 @@ dictionary. The ``convert_dict_qtables`` method no longer
performs any operations on the data given to it, has been deprecated and will be
removed in Pillow 10.0.0 (2023-07-01).
-API Changes
+API changes
===========
Changed WebP default "method" value when saving
@@ -73,7 +73,7 @@ through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GP
EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and
``im.getexif().get_ifd(0x8769)`` respectively.
-API Additions
+API additions
=============
ImageOps.contain
@@ -100,7 +100,7 @@ format, through the new ``bitmap_format`` argument::
im.save("out.ico", bitmap_format="bmp")
-Other Changes
+Other changes
=============
Added DDS BC5 reading and uncompressed saving
diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst
index 34ba703f7..e26a6ceda 100644
--- a/docs/releasenotes/8.3.2.rst
+++ b/docs/releasenotes/8.3.2.rst
@@ -20,7 +20,7 @@ bytes off the end of the allocated buffer from the heap. Present since Pillow 7.
This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs.
-Other Changes
+Other changes
=============
Python 3.10 wheels
diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst
index bdc8e8020..3bdf77d56 100644
--- a/docs/releasenotes/8.4.0.rst
+++ b/docs/releasenotes/8.4.0.rst
@@ -13,7 +13,7 @@ Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular length
default, and the size parameter could be used to override that. Pillow 8.3.0 removed
the default required length, also removing the need for the size parameter.
-API Additions
+API additions
=============
Added "transparency" argument for loading EPS images
@@ -33,7 +33,7 @@ Added WalImageFile class
:py:class:`PIL.Image.Image` instance. It now returns a dedicated
:py:class:`PIL.WalImageFile.WalImageFile` class.
-Other Changes
+Other changes
=============
Speed improvement when rotating square images
diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst
index fee66b6d0..660e5514c 100644
--- a/docs/releasenotes/9.0.0.rst
+++ b/docs/releasenotes/9.0.0.rst
@@ -59,7 +59,7 @@ initializing ``ImagePath.Path``.
.. _OSS-Fuzz: https://github.com/google/oss-fuzz
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
Python 3.6
@@ -102,7 +102,7 @@ ImageFile.raise_ioerror
has been removed. Use ``ImageFile.raise_oserror`` instead.
-API Changes
+API changes
===========
Added line width parameter to ImageDraw polygon
@@ -111,7 +111,7 @@ Added line width parameter to ImageDraw polygon
An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``.
-API Additions
+API additions
=============
ImageShow.XDGViewer
@@ -132,7 +132,7 @@ Support has been added for the "title" argument in
argument will also now be supported, e.g. ``im.show(title="My Image")`` and
``ImageShow.show(im, title="My Image")``.
-Other Changes
+Other changes
=============
Convert subsequent GIF frames to RGB or RGBA
diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst
index f65e3bcc2..5326afe78 100644
--- a/docs/releasenotes/9.0.1.rst
+++ b/docs/releasenotes/9.0.1.rst
@@ -21,7 +21,7 @@ While Pillow 9.0 restricted top-level builtins available to
:py:meth:`!PIL.ImageMath.eval`, it did not prevent builtins
available to lambda expressions. These are now also restricted.
-Other Changes
+Other changes
=============
Pillow 9.0 added support for ``xdg-open`` as an image viewer, but there have been
diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst
index 5b83d1e9c..72749ce8c 100644
--- a/docs/releasenotes/9.1.0.rst
+++ b/docs/releasenotes/9.1.0.rst
@@ -94,7 +94,7 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead.
-API Changes
+API changes
===========
Raise an error when performing a negative crop
@@ -137,7 +137,7 @@ On macOS, the last argument may need to be wrapped in quotes, e.g.
Therefore ``requirements.txt`` has been removed along with the ``make install-req``
command for installing its contents.
-API Additions
+API additions
=============
Added get_photoshop_blocks() to parse Photoshop TIFF tag
@@ -193,7 +193,7 @@ palette. ::
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
-Other Changes
+Other changes
=============
musllinux wheels
diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst
index 6e0647343..a3c9800b6 100644
--- a/docs/releasenotes/9.2.0.rst
+++ b/docs/releasenotes/9.2.0.rst
@@ -126,7 +126,7 @@ Use instead::
draw = ImageDraw.Draw(im)
draw.text((100 / 2, 100 / 2), "Hello world", font=font, anchor="mm")
-API Additions
+API additions
=============
Image.apply_transparency
@@ -137,7 +137,7 @@ with "transparency" in ``im.info``, and apply the transparency to the palette in
The image's palette mode will become "RGBA", and "transparency" will be removed from
``im.info``.
-Other Changes
+Other changes
=============
Using gnome-screenshot on Linux
diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst
index e5987ce08..bb1e731fd 100644
--- a/docs/releasenotes/9.3.0.rst
+++ b/docs/releasenotes/9.3.0.rst
@@ -28,7 +28,7 @@ This was introduced in Pillow 9.2.0, found with `OSS-Fuzz`_ and fixed by limitin
``SAMPLESPERPIXEL`` to the number of planes that we can decode.
-API Additions
+API additions
=============
Allow default ImageDraw font to be set
@@ -65,7 +65,7 @@ The data from :py:data:`~PIL.ExifTags.TAGS` and
:py:data:`~PIL.ExifTags.GPS`.
-Other Changes
+Other changes
=============
Python 3.11 wheels
diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst
index 37f26a22c..3b202157d 100644
--- a/docs/releasenotes/9.4.0.rst
+++ b/docs/releasenotes/9.4.0.rst
@@ -20,7 +20,7 @@ Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a
crash. An error is now raised instead. This has been present since
Pillow 8.0.0.
-API Additions
+API additions
=============
Added start position for getmask and getmask2
@@ -88,7 +88,7 @@ When saving a JPEG image, a comment can now be written from
im.save(out, comment="Test comment")
-Other Changes
+Other changes
=============
Added support for DDS L and LA images
diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst
index 501479bb6..6bf2079c8 100644
--- a/docs/releasenotes/9.5.0.rst
+++ b/docs/releasenotes/9.5.0.rst
@@ -37,7 +37,7 @@ be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
be used internally, so there is no replacement. If you need this functionality
though, it is a very short class that can easily be recreated in your own code.
-API Additions
+API additions
=============
QOI file format
@@ -71,7 +71,7 @@ If OpenJPEG 2.4.0 or later is available and the ``plt`` keyword argument
is present and true when saving JPEG2000 images, tell the encoder to generate
PLT markers.
-Other Changes
+Other changes
=============
Added support for saving PDFs in RGBA mode
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index be9f623d0..5d7b21d59 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -1,4 +1,4 @@
-Release Notes
+Release notes
=============
Pillow is released quarterly on January 2nd, April 1st, July 1st and October 15th.
@@ -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
diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst
index cfc7221a3..a453d2a43 100644
--- a/docs/releasenotes/template.rst
+++ b/docs/releasenotes/template.rst
@@ -14,7 +14,7 @@ TODO
TODO
-Backwards Incompatible Changes
+Backwards incompatible changes
==============================
TODO
@@ -28,7 +28,7 @@ TODO
TODO
-API Changes
+API changes
===========
TODO
@@ -36,7 +36,7 @@ TODO
TODO
-API Additions
+API additions
=============
TODO
@@ -44,7 +44,7 @@ TODO
TODO
-Other Changes
+Other changes
=============
TODO
diff --git a/pyproject.toml b/pyproject.toml
index 2ffd9faca..a3ff9723b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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'",
]
@@ -121,6 +127,7 @@ lint.select = [
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks
+ "PIE", # flake8-pie
"PT", # flake8-pytest-style
"PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa)
@@ -133,6 +140,7 @@ lint.ignore = [
"E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ','
+ "PIE790", # flake8-pie: unnecessary-placeholder
"PT001", # pytest-fixture-incorrect-parentheses-style
"PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad
diff --git a/setup.py b/setup.py
index a85731db9..5ecd6b816 100644
--- a/setup.py
+++ b/setup.py
@@ -32,6 +32,7 @@ configuration: dict[str, list[str]] = {}
PILLOW_VERSION = get_version()
+AVIF_ROOT = None
FREETYPE_ROOT = None
HARFBUZZ_ROOT = None
FRIBIDI_ROOT = None
@@ -64,10 +65,12 @@ _IMAGING = ("decode", "encode", "map", "display", "outline", "path")
_LIB_IMAGING = (
"Access",
"AlphaComposite",
+ "Arrow",
"Resample",
"Reduce",
"Bands",
"BcnDecode",
+ "BcnEncode",
"BitDecode",
"Blend",
"Chops",
@@ -305,6 +308,7 @@ class pil_build_ext(build_ext):
"jpeg2000",
"imagequant",
"xcb",
+ "avif",
]
required = {"jpeg", "zlib"}
@@ -480,6 +484,7 @@ class pil_build_ext(build_ext):
#
# add configured kits
for root_name, lib_name in {
+ "AVIF_ROOT": "avif",
"JPEG_ROOT": "libjpeg",
"JPEG2K_ROOT": "libopenjp2",
"TIFF_ROOT": ("libtiff-5", "libtiff-4"),
@@ -845,6 +850,12 @@ class pil_build_ext(build_ext):
if _find_library_file(self, "xcb"):
feature.set("xcb", "xcb")
+ if feature.want("avif"):
+ _dbg("Looking for avif")
+ if _find_include_file(self, "avif/avif.h"):
+ if _find_library_file(self, "avif"):
+ feature.set("avif", "avif")
+
for f in feature:
if not feature.get(f) and feature.require(f):
if f in ("jpeg", "zlib"):
@@ -933,6 +944,14 @@ class pil_build_ext(build_ext):
else:
self._remove_extension("PIL._webp")
+ if feature.get("avif"):
+ libs = [feature.get("avif")]
+ if sys.platform == "win32":
+ libs.extend(["ntdll", "userenv", "ws2_32", "bcrypt"])
+ self._update_extension("PIL._avif", libs)
+ else:
+ self._remove_extension("PIL._avif")
+
tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else []
self._update_extension("PIL._imagingtk", tk_libs)
@@ -975,6 +994,7 @@ class pil_build_ext(build_ext):
(feature.get("lcms"), "LITTLECMS2"),
(feature.get("webp"), "WEBP"),
(feature.get("xcb"), "XCB (X protocol)"),
+ (feature.get("avif"), "LIBAVIF"),
]
all = 1
@@ -1017,6 +1037,7 @@ ext_modules = [
Extension("PIL._imagingft", ["src/_imagingft.c"]),
Extension("PIL._imagingcms", ["src/_imagingcms.c"]),
Extension("PIL._webp", ["src/_webp.c"]),
+ Extension("PIL._avif", ["src/_avif.c"]),
Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]),
Extension("PIL._imagingmath", ["src/_imagingmath.c"]),
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
diff --git a/src/PIL/AvifImagePlugin.py b/src/PIL/AvifImagePlugin.py
new file mode 100644
index 000000000..b2c5ab15d
--- /dev/null
+++ b/src/PIL/AvifImagePlugin.py
@@ -0,0 +1,292 @@
+from __future__ import annotations
+
+import os
+from io import BytesIO
+from typing import IO
+
+from . import ExifTags, Image, ImageFile
+
+try:
+ from . import _avif
+
+ SUPPORTED = True
+except ImportError:
+ SUPPORTED = False
+
+# Decoder options as module globals, until there is a way to pass parameters
+# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
+DECODE_CODEC_CHOICE = "auto"
+# Decoding is only affected by this for libavif **0.8.4** or greater.
+DEFAULT_MAX_THREADS = 0
+
+
+def get_codec_version(codec_name: str) -> str | None:
+ versions = _avif.codec_versions()
+ for version in versions.split(", "):
+ if version.split(" [")[0] == codec_name:
+ return version.split(":")[-1].split(" ")[0]
+ return None
+
+
+def _accept(prefix: bytes) -> bool | str:
+ if prefix[4:8] != b"ftyp":
+ return False
+ major_brand = prefix[8:12]
+ if major_brand in (
+ # coding brands
+ b"avif",
+ b"avis",
+ # We accept files with AVIF container brands; we can't yet know if
+ # the ftyp box has the correct compatible brands, but if it doesn't
+ # then the plugin will raise a SyntaxError which Pillow will catch
+ # before moving on to the next plugin that accepts the file.
+ #
+ # Also, because this file might not actually be an AVIF file, we
+ # don't raise an error if AVIF support isn't properly compiled.
+ b"mif1",
+ b"msf1",
+ ):
+ if not SUPPORTED:
+ return (
+ "image file could not be identified because AVIF support not installed"
+ )
+ return True
+ return False
+
+
+def _get_default_max_threads() -> int:
+ if DEFAULT_MAX_THREADS:
+ return DEFAULT_MAX_THREADS
+ if hasattr(os, "sched_getaffinity"):
+ return len(os.sched_getaffinity(0))
+ else:
+ return os.cpu_count() or 1
+
+
+class AvifImageFile(ImageFile.ImageFile):
+ format = "AVIF"
+ format_description = "AVIF image"
+ __frame = -1
+
+ def _open(self) -> None:
+ if not SUPPORTED:
+ msg = "image file could not be opened because AVIF support not installed"
+ raise SyntaxError(msg)
+
+ if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available(
+ DECODE_CODEC_CHOICE
+ ):
+ msg = "Invalid opening codec"
+ raise ValueError(msg)
+ self._decoder = _avif.AvifDecoder(
+ self.fp.read(),
+ DECODE_CODEC_CHOICE,
+ _get_default_max_threads(),
+ )
+
+ # Get info from decoder
+ self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = (
+ self._decoder.get_info()
+ )
+ self.is_animated = self.n_frames > 1
+
+ if icc:
+ self.info["icc_profile"] = icc
+ if xmp:
+ self.info["xmp"] = xmp
+
+ if exif_orientation != 1 or exif:
+ exif_data = Image.Exif()
+ if exif:
+ exif_data.load(exif)
+ original_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
+ else:
+ original_orientation = 1
+ if exif_orientation != original_orientation:
+ exif_data[ExifTags.Base.Orientation] = exif_orientation
+ exif = exif_data.tobytes()
+ if exif:
+ self.info["exif"] = exif
+ self.seek(0)
+
+ def seek(self, frame: int) -> None:
+ if not self._seek_check(frame):
+ return
+
+ # Set tile
+ self.__frame = frame
+ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
+
+ def load(self) -> Image.core.PixelAccess | None:
+ if self.tile:
+ # We need to load the image data for this frame
+ data, timescale, pts_in_timescales, duration_in_timescales = (
+ self._decoder.get_frame(self.__frame)
+ )
+ self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
+ self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
+
+ if self.fp and self._exclusive_fp:
+ self.fp.close()
+ self.fp = BytesIO(data)
+
+ return super().load()
+
+ def load_seek(self, pos: int) -> None:
+ pass
+
+ def tell(self) -> int:
+ return self.__frame
+
+
+def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ _save(im, fp, filename, save_all=True)
+
+
+def _save(
+ im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
+) -> None:
+ info = im.encoderinfo.copy()
+ if save_all:
+ append_images = list(info.get("append_images", []))
+ else:
+ append_images = []
+
+ total = 0
+ for ims in [im] + append_images:
+ total += getattr(ims, "n_frames", 1)
+
+ quality = info.get("quality", 75)
+ if not isinstance(quality, int) or quality < 0 or quality > 100:
+ msg = "Invalid quality setting"
+ raise ValueError(msg)
+
+ duration = info.get("duration", 0)
+ subsampling = info.get("subsampling", "4:2:0")
+ speed = info.get("speed", 6)
+ max_threads = info.get("max_threads", _get_default_max_threads())
+ codec = info.get("codec", "auto")
+ if codec != "auto" and not _avif.encoder_codec_available(codec):
+ msg = "Invalid saving codec"
+ raise ValueError(msg)
+ range_ = info.get("range", "full")
+ tile_rows_log2 = info.get("tile_rows", 0)
+ tile_cols_log2 = info.get("tile_cols", 0)
+ alpha_premultiplied = bool(info.get("alpha_premultiplied", False))
+ autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0))
+
+ icc_profile = info.get("icc_profile", im.info.get("icc_profile"))
+ exif_orientation = 1
+ if exif := info.get("exif"):
+ if isinstance(exif, Image.Exif):
+ exif_data = exif
+ else:
+ exif_data = Image.Exif()
+ exif_data.load(exif)
+ if ExifTags.Base.Orientation in exif_data:
+ exif_orientation = exif_data.pop(ExifTags.Base.Orientation)
+ exif = exif_data.tobytes() if exif_data else b""
+ elif isinstance(exif, Image.Exif):
+ exif = exif_data.tobytes()
+
+ xmp = info.get("xmp")
+
+ if isinstance(xmp, str):
+ xmp = xmp.encode("utf-8")
+
+ advanced = info.get("advanced")
+ if advanced is not None:
+ if isinstance(advanced, dict):
+ advanced = advanced.items()
+ try:
+ advanced = tuple(advanced)
+ except TypeError:
+ invalid = True
+ else:
+ invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
+ if invalid:
+ msg = (
+ "advanced codec options must be a dict of key-value string "
+ "pairs or a series of key-value two-tuples"
+ )
+ raise ValueError(msg)
+
+ # Setup the AVIF encoder
+ enc = _avif.AvifEncoder(
+ im.size,
+ subsampling,
+ quality,
+ speed,
+ max_threads,
+ codec,
+ range_,
+ tile_rows_log2,
+ tile_cols_log2,
+ alpha_premultiplied,
+ autotiling,
+ icc_profile or b"",
+ exif or b"",
+ exif_orientation,
+ xmp or b"",
+ advanced,
+ )
+
+ # Add each frame
+ frame_idx = 0
+ frame_duration = 0
+ cur_idx = im.tell()
+ is_single_frame = total == 1
+ try:
+ for ims in [im] + append_images:
+ # Get number of frames in this image
+ nfr = getattr(ims, "n_frames", 1)
+
+ for idx in range(nfr):
+ ims.seek(idx)
+
+ # Make sure image mode is supported
+ frame = ims
+ rawmode = ims.mode
+ if ims.mode not in {"RGB", "RGBA"}:
+ rawmode = "RGBA" if ims.has_transparency_data else "RGB"
+ frame = ims.convert(rawmode)
+
+ # Update frame duration
+ if isinstance(duration, (list, tuple)):
+ frame_duration = duration[frame_idx]
+ else:
+ frame_duration = duration
+
+ # Append the frame to the animation encoder
+ enc.add(
+ frame.tobytes("raw", rawmode),
+ frame_duration,
+ frame.size,
+ rawmode,
+ is_single_frame,
+ )
+
+ # Update frame index
+ frame_idx += 1
+
+ if not save_all:
+ break
+
+ finally:
+ im.seek(cur_idx)
+
+ # Get the final output from the encoder
+ data = enc.finish()
+ if data is None:
+ msg = "cannot write file as AVIF (encoder returned None)"
+ raise OSError(msg)
+
+ fp.write(data)
+
+
+Image.register_open(AvifImageFile.format, AvifImageFile, _accept)
+if SUPPORTED:
+ Image.register_save(AvifImageFile.format, _save)
+ Image.register_save_all(AvifImageFile.format, _save_all)
+ Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"])
+ Image.register_mime(AvifImageFile.format, "image/avif")
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 5747c1252..f7be7746d 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -291,7 +291,7 @@ class BlpImageFile(ImageFile.ImageFile):
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
-class _BLPBaseDecoder(ImageFile.PyDecoder):
+class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index 14410f7eb..345cc24ab 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -48,6 +48,8 @@ BIT2MODE = {
32: ("RGB", "BGRX"),
}
+USE_RAW_ALPHA = False
+
def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"BM")
@@ -242,11 +244,8 @@ class BmpImageFile(ImageFile.ImageFile):
msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
- try:
- if file_info["bits"] == 32 and self.alpha:
- raw_mode, self._mode = "BGRA", "RGBA"
- except AttributeError:
- pass
+ if file_info["bits"] == 32 and (USE_RAW_ALPHA or hasattr(self, "alpha")):
+ raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in (
self.COMPRESSIONS["RLE8"],
self.COMPRESSIONS["RLE4"],
diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py
index f67f27d73..aea661b9c 100644
--- a/src/PIL/DcxImagePlugin.py
+++ b/src/PIL/DcxImagePlugin.py
@@ -24,6 +24,7 @@ from __future__ import annotations
from . import Image
from ._binary import i32le as i32
+from ._util import DeferredError
from .PcxImagePlugin import PcxImageFile
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
@@ -66,6 +67,8 @@ class DcxImageFile(PcxImageFile):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
self.frame = frame
self.fp = self._fp
self.fp.seek(self._offset[frame])
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index cdae8dfee..26307817c 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -419,6 +419,14 @@ class DdsImageFile(ImageFile.ImageFile):
self._mode = "RGBA"
self.pixel_format = "BC1"
n = 1
+ elif dxgi_format in (DXGI_FORMAT.BC2_TYPELESS, DXGI_FORMAT.BC2_UNORM):
+ self._mode = "RGBA"
+ self.pixel_format = "BC2"
+ n = 2
+ elif dxgi_format in (DXGI_FORMAT.BC3_TYPELESS, DXGI_FORMAT.BC3_UNORM):
+ self._mode = "RGBA"
+ self.pixel_format = "BC3"
+ n = 3
elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM):
self._mode = "L"
self.pixel_format = "BC4"
@@ -518,30 +526,68 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg)
- alpha = im.mode[-1] == "A"
- if im.mode[0] == "L":
- pixel_flags = DDPF.LUMINANCE
- rawmode = im.mode
- if alpha:
- rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
- else:
- rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
- else:
- pixel_flags = DDPF.RGB
- rawmode = im.mode[::-1]
- rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
-
- if alpha:
- r, g, b, a = im.split()
- im = Image.merge("RGBA", (a, r, g, b))
- if alpha:
- pixel_flags |= DDPF.ALPHAPIXELS
- rgba_mask.append(0xFF000000 if alpha else 0)
-
- flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT
+ flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
bitcount = len(im.getbands()) * 8
- pitch = (im.width * bitcount + 7) // 8
+ pixel_format = im.encoderinfo.get("pixel_format")
+ args: tuple[int] | str
+ if pixel_format:
+ codec_name = "bcn"
+ flags |= DDSD.LINEARSIZE
+ pitch = (im.width + 3) * 4
+ rgba_mask = [0, 0, 0, 0]
+ pixel_flags = DDPF.FOURCC
+ if pixel_format == "DXT1":
+ fourcc = D3DFMT.DXT1
+ args = (1,)
+ elif pixel_format == "DXT3":
+ fourcc = D3DFMT.DXT3
+ args = (2,)
+ elif pixel_format == "DXT5":
+ fourcc = D3DFMT.DXT5
+ args = (3,)
+ else:
+ fourcc = D3DFMT.DX10
+ if pixel_format == "BC2":
+ args = (2,)
+ dxgi_format = DXGI_FORMAT.BC2_TYPELESS
+ elif pixel_format == "BC3":
+ args = (3,)
+ dxgi_format = DXGI_FORMAT.BC3_TYPELESS
+ elif pixel_format == "BC5":
+ args = (5,)
+ dxgi_format = DXGI_FORMAT.BC5_TYPELESS
+ if im.mode != "RGB":
+ msg = "only RGB mode can be written as BC5"
+ raise OSError(msg)
+ else:
+ msg = f"cannot write pixel format {pixel_format}"
+ raise OSError(msg)
+ else:
+ codec_name = "raw"
+ flags |= DDSD.PITCH
+ pitch = (im.width * bitcount + 7) // 8
+ alpha = im.mode[-1] == "A"
+ if im.mode[0] == "L":
+ pixel_flags = DDPF.LUMINANCE
+ args = im.mode
+ if alpha:
+ rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
+ else:
+ rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
+ else:
+ pixel_flags = DDPF.RGB
+ args = im.mode[::-1]
+ rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
+
+ if alpha:
+ r, g, b, a = im.split()
+ im = Image.merge("RGBA", (a, r, g, b))
+ if alpha:
+ pixel_flags |= DDPF.ALPHAPIXELS
+ rgba_mask.append(0xFF000000 if alpha else 0)
+
+ fourcc = D3DFMT.UNKNOWN
fp.write(
o32(DDS_MAGIC)
+ struct.pack(
@@ -556,11 +602,16 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
)
+ struct.pack("11I", *((0,) * 11)) # reserved
# pfsize, pfflags, fourcc, bitcount
- + struct.pack("<4I", 32, pixel_flags, 0, bitcount)
+ + struct.pack("<4I", 32, pixel_flags, fourcc, bitcount)
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
)
- ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
+ if fourcc == D3DFMT.DX10:
+ fp.write(
+ # dxgi_format, 2D resource, misc, array size, straight alpha
+ struct.pack("<5I", dxgi_format, 3, 0, 0, 1)
+ )
+ ImageFile._save(im, fp, [ImageFile._Tile(codec_name, (0, 0) + im.size, 0, args)])
def _accept(prefix: bytes) -> bool:
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index b534b30ab..7c5bfeefa 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
from ._binary import i32le as i32
from ._binary import o8
+from ._util import DeferredError
#
# decoder
@@ -134,6 +135,8 @@ class FliImageFile(ImageFile.ImageFile):
self._seek(f)
def _seek(self, frame: int) -> None:
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
if frame == 0:
self.__frame = -1
self._fp.seek(self.__rewind)
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 259e93f09..4392c4cb9 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -31,7 +31,7 @@ import os
import subprocess
from enum import IntEnum
from functools import cached_property
-from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union
+from typing import IO, Any, Literal, NamedTuple, Union
from . import (
Image,
@@ -45,7 +45,9 @@ from . import (
from ._binary import i16le as i16
from ._binary import o8
from ._binary import o16le as o16
+from ._util import DeferredError
+TYPE_CHECKING = False
if TYPE_CHECKING:
from . import _imaging
from ._typing import Buffer
@@ -167,6 +169,8 @@ class GifImageFile(ImageFile.ImageFile):
raise EOFError(msg) from e
def _seek(self, frame: int, update_image: bool = True) -> None:
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
if frame == 0:
# rewind
self.__offset = 0
diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py
index 1b7a394c0..379ffd739 100644
--- a/src/PIL/GimpPaletteFile.py
+++ b/src/PIL/GimpPaletteFile.py
@@ -16,24 +16,27 @@
from __future__ import annotations
import re
+from io import BytesIO
from typing import IO
-from ._binary import o8
-
class GimpPaletteFile:
"""File handler for GIMP's palette format."""
rawmode = "RGB"
- def __init__(self, fp: IO[bytes]) -> None:
- palette = [o8(i) * 3 for i in range(256)]
-
+ def _read(self, fp: IO[bytes], limit: bool = True) -> None:
if not fp.readline().startswith(b"GIMP Palette"):
msg = "not a GIMP palette file"
raise SyntaxError(msg)
- for i in range(256):
+ palette: list[int] = []
+ i = 0
+ while True:
+ if limit and i == 256 + 3:
+ break
+
+ i += 1
s = fp.readline()
if not s:
break
@@ -41,18 +44,29 @@ class GimpPaletteFile:
# skip fields and comment lines
if re.match(rb"\w+:|#", s):
continue
- if len(s) > 100:
+ if limit and len(s) > 100:
msg = "bad palette file"
raise SyntaxError(msg)
- v = tuple(map(int, s.split()[:3]))
- if len(v) != 3:
+ v = s.split(maxsplit=3)
+ if len(v) < 3:
msg = "bad palette entry"
raise ValueError(msg)
- palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
+ palette += (int(v[i]) for i in range(3))
+ if limit and len(palette) == 768:
+ break
- self.palette = b"".join(palette)
+ self.palette = bytes(palette)
+
+ def __init__(self, fp: IO[bytes]) -> None:
+ self._read(fp)
+
+ @classmethod
+ def frombytes(cls, data: bytes) -> GimpPaletteFile:
+ self = cls.__new__(cls)
+ self._read(BytesIO(data), False)
+ return self
def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index a5d5b93ae..5a88429e5 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -123,8 +123,7 @@ def read_png_or_jpeg2000(
Image._decompression_bomb_check(im.size)
return {"RGBA": im}
elif (
- sig.startswith(b"\xff\x4f\xff\x51")
- or sig.startswith(b"\x0d\x0a\x87\x0a")
+ sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a"))
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
):
if not enable_jpeg2k:
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 9f20b30f8..71b999678 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -31,6 +31,7 @@ import re
from typing import IO, Any
from . import Image, ImageFile, ImagePalette
+from ._util import DeferredError
# --------------------------------------------------------------------
# Standard tags
@@ -290,6 +291,8 @@ class ImImageFile(ImageFile.ImageFile):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
self.frame = frame
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 6a2aa3e4c..ded40bc5d 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -41,14 +41,7 @@ import warnings
from collections.abc import Callable, Iterator, MutableMapping, Sequence
from enum import IntEnum
from types import ModuleType
-from typing import (
- IO,
- TYPE_CHECKING,
- Any,
- Literal,
- Protocol,
- cast,
-)
+from typing import IO, Any, Literal, Protocol, cast
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
@@ -218,6 +211,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# --------------------------------------------------------------------
# Registries
+TYPE_CHECKING = False
if TYPE_CHECKING:
import mmap
from xml.etree.ElementTree import Element
@@ -548,7 +542,6 @@ class Image:
def __init__(self) -> None:
# FIXME: take "new" parameters / other image?
- # FIXME: turn mode and size into delegating properties?
self._im: core.ImagingCore | DeferredError | None = None
self._mode = ""
self._size = (0, 0)
@@ -584,6 +577,14 @@ class Image:
def mode(self) -> str:
return self._mode
+ @property
+ def readonly(self) -> int:
+ return (self._im and self._im.readonly) or self._readonly
+
+ @readonly.setter
+ def readonly(self, readonly: int) -> None:
+ self._readonly = readonly
+
def _new(self, im: core.ImagingCore) -> Image:
new = Image()
new.im = im
@@ -622,6 +623,8 @@ class Image:
more information.
"""
if getattr(self, "map", None):
+ if sys.platform == "win32" and hasattr(sys, "pypy_version_info"):
+ self.map.close()
self.map: mmap.mmap | None = None
# Instead of simply setting to None, we're setting up a
@@ -733,6 +736,16 @@ class Image:
new["shape"], new["typestr"] = _conv_type_shape(self)
return new
+ def __arrow_c_schema__(self) -> object:
+ self.load()
+ return self.im.__arrow_c_schema__()
+
+ def __arrow_c_array__(
+ self, requested_schema: object | None = None
+ ) -> tuple[object, object]:
+ self.load()
+ return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__())
+
def __getstate__(self) -> list[Any]:
im_data = self.tobytes() # load image first
return [self.info, self.mode, self.size, self.getpalette(), im_data]
@@ -1001,7 +1014,7 @@ class Image:
elif len(mode) == 3:
transparency = tuple(
convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
- for i in range(0, len(transparency))
+ for i in range(len(transparency))
)
new_im.info["transparency"] = transparency
return new_im
@@ -1525,6 +1538,8 @@ class Image:
# XMP tags
if ExifTags.Base.Orientation not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
+ if not xmp_tags and (xmp_tags := self.info.get("xmp")):
+ xmp_tags = xmp_tags.decode("utf-8")
if xmp_tags:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match:
@@ -2475,7 +2490,21 @@ class Image:
format to use is determined from the filename extension.
If a file object was used instead of a filename, this
parameter should always be used.
- :param params: Extra parameters to the image writer.
+ :param params: Extra parameters to the image writer. These can also be
+ set on the image itself through ``encoderinfo``. This is useful when
+ saving multiple images::
+
+ # Saving XMP data to a single image
+ from PIL import Image
+ red = Image.new("RGB", (1, 1), "#f00")
+ red.save("out.mpo", xmp=b"test")
+
+ # Saving XMP data to the second frame of an image
+ from PIL import Image
+ black = Image.new("RGB", (1, 1))
+ red = Image.new("RGB", (1, 1), "#f00")
+ red.encoderinfo = {"xmp": b"test"}
+ black.save("out.mpo", save_all=True, append_images=[red])
:returns: None
:exception ValueError: If the output format could not be determined
from the file name. Use the format option to solve this.
@@ -2497,13 +2526,6 @@ class Image:
# only set the name for metadata purposes
filename = os.fspath(fp.name)
- # may mutate self!
- self._ensure_mutable()
-
- save_all = params.pop("save_all", False)
- self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params}
- self.encoderconfig: tuple[Any, ...] = ()
-
preinit()
filename_ext = os.path.splitext(filename)[1].lower()
@@ -2518,9 +2540,27 @@ class Image:
msg = f"unknown file extension: {ext}"
raise ValueError(msg) from e
+ from . import ImageFile
+
+ # may mutate self!
+ if isinstance(self, ImageFile.ImageFile) and os.path.abspath(
+ filename
+ ) == os.path.abspath(self.filename):
+ self._ensure_mutable()
+ else:
+ self.load()
+
+ save_all = params.pop("save_all", None)
+ self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params}
+ self.encoderconfig: tuple[Any, ...] = ()
+
if format.upper() not in SAVE:
init()
- if save_all:
+ if save_all or (
+ save_all is None
+ and params.get("append_images")
+ and format.upper() in SAVE_ALL
+ ):
save_handler = SAVE_ALL[format.upper()]
else:
save_handler = SAVE[format.upper()]
@@ -2966,7 +3006,7 @@ class Image:
# Abstract handlers.
-class ImagePointHandler:
+class ImagePointHandler(abc.ABC):
"""
Used as a mixin by point transforms
(for use with :py:meth:`~PIL.Image.Image.point`)
@@ -2977,7 +3017,7 @@ class ImagePointHandler:
pass
-class ImageTransformHandler:
+class ImageTransformHandler(abc.ABC):
"""
Used as a mixin by geometry transforms
(for use with :py:meth:`~PIL.Image.Image.transform`)
@@ -3186,6 +3226,18 @@ class SupportsArrayInterface(Protocol):
raise NotImplementedError()
+class SupportsArrowArrayInterface(Protocol):
+ """
+ An object that has an ``__arrow_c_array__`` method corresponding to the arrow c
+ data interface.
+ """
+
+ def __arrow_c_array__(
+ self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037
+ ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037
+ raise NotImplementedError()
+
+
def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
"""
Creates an image memory from an object exporting the array interface
@@ -3274,6 +3326,58 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
+def fromarrow(
+ obj: SupportsArrowArrayInterface, mode: str, size: tuple[int, int]
+) -> Image:
+ """Creates 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))
+
+ If the data representation of the ``obj`` is not compatible with
+ Pillow internal storage, a ValueError is raised.
+
+ 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)
+
+ As with array support, when converting Pillow images to arrays,
+ only pixel values are transferred. This means that P and PA mode
+ images will lose their palette.
+
+ :param obj: Object with an arrow_c_array interface
+ :param mode: Image mode.
+ :param size: Image size. This must match the storage of the arrow object.
+ :returns: An Image object
+
+ Note that according to the Arrow spec, both the producer and the
+ consumer should consider the exported array to be immutable, as
+ unsynchronized updates will potentially cause inconsistent data.
+
+ See: :ref:`arrow-support` for more detailed information
+
+ .. versionadded:: 11.2.1
+
+ """
+ if not hasattr(obj, "__arrow_c_array__"):
+ msg = "arrow_c_array interface not found"
+ raise ValueError(msg)
+
+ (schema_capsule, array_capsule) = obj.__arrow_c_array__()
+ _im = core.new_arrow(mode, size, schema_capsule, array_capsule)
+ if _im:
+ return Image()._new(_im)
+
+ msg = "new_arrow returned None without an exception"
+ raise ValueError(msg)
+
+
def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile:
"""Creates an image instance from a QImage image"""
from . import ImageQt
@@ -4003,7 +4107,7 @@ class Exif(_ExifBase):
ifd_data = tag_data[ifd_offset:]
makernote = {}
- for i in range(0, struct.unpack("H", tag_data[:2])[0]):
+ for i in range(struct.unpack(">H", tag_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index c4ebc5931..e6c7b0298 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -35,7 +35,7 @@ import math
import struct
from collections.abc import Sequence
from types import ModuleType
-from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast
+from typing import Any, AnyStr, Callable, Union, cast
from . import Image, ImageColor
from ._deprecate import deprecate
@@ -44,6 +44,7 @@ from ._typing import Coords
# experimental access to the outline API
Outline: Callable[[], Image.core._Outline] = Image.core.outline
+TYPE_CHECKING = False
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont
@@ -1204,7 +1205,7 @@ def _compute_regular_polygon_vertices(
degrees = 360 / n_sides
# Start with the bottom left polygon vertex
current_angle = (270 - 0.5 * degrees) + rotation
- for _ in range(0, n_sides):
+ for _ in range(n_sides):
angles.append(current_angle)
current_angle += degrees
if current_angle > 360:
@@ -1227,4 +1228,4 @@ def _color_diff(
first = color1 if isinstance(color1, tuple) else (color1,)
second = color2 if isinstance(color2, tuple) else (color2,)
- return sum(abs(first[i] - second[i]) for i in range(0, len(second)))
+ return sum(abs(first[i] - second[i]) for i in range(len(second)))
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index c3901d488..bcb7d462e 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -34,13 +34,13 @@ import itertools
import logging
import os
import struct
-import sys
-from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
+from typing import IO, Any, NamedTuple, cast
from . import ExifTags, Image
from ._deprecate import deprecate
from ._util import DeferredError, is_path
+TYPE_CHECKING = False
if TYPE_CHECKING:
from ._typing import StrOrBytesPath
@@ -167,7 +167,7 @@ class ImageFile(Image.Image):
pass
def _close_fp(self):
- if getattr(self, "_fp", False):
+ if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError):
if self._fp != self.fp:
self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image"))
@@ -252,8 +252,12 @@ class ImageFile(Image.Image):
return Image.MIME.get(self.format.upper())
return None
+ def __getstate__(self) -> list[Any]:
+ return super().__getstate__() + [self.filename]
+
def __setstate__(self, state: list[Any]) -> None:
self.tile = []
+ self.filename = state[5]
super().__setstate__(state)
def verify(self) -> None:
@@ -278,8 +282,6 @@ class ImageFile(Image.Image):
self.map: mmap.mmap | None = None
use_mmap = self.filename and len(self.tile) == 1
- # As of pypy 2.1.0, memory mapping was failing here.
- use_mmap = use_mmap and not hasattr(sys, "pypy_version_info")
readonly = 0
@@ -345,7 +347,7 @@ class ImageFile(Image.Image):
self.tile, lambda tile: (tile[0], tile[1], tile[3])
)
]
- for decoder_name, extents, offset, args in self.tile:
+ for i, (decoder_name, extents, offset, args) in enumerate(self.tile):
seek(offset)
decoder = Image._getdecoder(
self.mode, decoder_name, args, self.decoderconfig
@@ -358,8 +360,13 @@ class ImageFile(Image.Image):
else:
b = prefix
while True:
+ read_bytes = self.decodermaxblock
+ if i + 1 < len(self.tile):
+ next_offset = self.tile[i + 1].offset
+ if next_offset > offset:
+ read_bytes = next_offset - offset
try:
- s = read(self.decodermaxblock)
+ s = read(read_bytes)
except (IndexError, struct.error) as e:
# truncated png/gif
if LOAD_TRUNCATED_IMAGES:
@@ -438,7 +445,7 @@ class ImageFile(Image.Image):
return self.tell() != frame
-class StubHandler:
+class StubHandler(abc.ABC):
def open(self, im: StubImageFile) -> None:
pass
@@ -447,7 +454,7 @@ class StubHandler:
pass
-class StubImageFile(ImageFile):
+class StubImageFile(ImageFile, metaclass=abc.ABCMeta):
"""
Base class for stub image loaders.
@@ -455,9 +462,9 @@ class StubImageFile(ImageFile):
certain format, but relies on external code to load the file.
"""
+ @abc.abstractmethod
def _open(self) -> None:
- msg = "StubImageFile subclass must implement _open"
- raise NotImplementedError(msg)
+ pass
def load(self) -> Image.core.PixelAccess | None:
loader = self._load()
@@ -471,10 +478,10 @@ class StubImageFile(ImageFile):
self.__dict__ = image.__dict__
return image.load()
+ @abc.abstractmethod
def _load(self) -> StubHandler | None:
"""(Hook) Find actual image loader."""
- msg = "StubImageFile subclass must implement _load"
- raise NotImplementedError(msg)
+ pass
class Parser:
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 1c8b29b11..b9ed54ab2 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -20,14 +20,15 @@ import abc
import functools
from collections.abc import Sequence
from types import ModuleType
-from typing import TYPE_CHECKING, Any, Callable, cast
+from typing import Any, Callable, cast
+TYPE_CHECKING = False
if TYPE_CHECKING:
from . import _imaging
from ._typing import NumpyArray
-class Filter:
+class Filter(abc.ABC):
@abc.abstractmethod
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
pass
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index c8f05fbb7..329c463ff 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -34,12 +34,13 @@ import warnings
from enum import IntEnum
from io import BytesIO
from types import ModuleType
-from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast
+from typing import IO, Any, BinaryIO, TypedDict, cast
from . import Image, features
from ._typing import StrOrBytesPath
from ._util import DeferredError, is_path
+TYPE_CHECKING = False
if TYPE_CHECKING:
from . import ImageFile
from ._imaging import ImagingFont
@@ -1092,7 +1093,7 @@ w7IkEbzhVQAAAABJRU5ErkJggg==
def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""If FreeType support is available, load a version of Aileron Regular,
- https://dotcolon.net/font/aileron, with a more limited character set.
+ https://dotcolon.net/fonts/aileron, with a more limited character set.
Otherwise, load a "better than nothing" font.
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index fe27bfaeb..c29350b7a 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -25,12 +25,17 @@ import tempfile
from . import Image
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from . import ImageWin
+
def grab(
bbox: tuple[int, int, int, int] | None = None,
include_layered_windows: bool = False,
all_screens: bool = False,
xdisplay: str | None = None,
+ window: int | ImageWin.HWND | None = None,
) -> Image.Image:
im: Image.Image
if xdisplay is None:
@@ -51,8 +56,12 @@ def grab(
return im_resized
return im
elif sys.platform == "win32":
+ if window is not None:
+ all_screens = -1
offset, size, data = Image.core.grabscreen_win32(
- include_layered_windows, all_screens
+ include_layered_windows,
+ all_screens,
+ int(window) if window is not None else 0,
)
im = Image.frombytes(
"RGB",
@@ -77,14 +86,18 @@ def grab(
raise OSError(msg)
size, data = Image.core.grabscreen_x11(display_name)
except OSError:
- if (
- display_name is None
- and sys.platform not in ("darwin", "win32")
- and shutil.which("gnome-screenshot")
- ):
+ if display_name is None and sys.platform not in ("darwin", "win32"):
+ if shutil.which("gnome-screenshot"):
+ args = ["gnome-screenshot", "-f"]
+ elif shutil.which("grim"):
+ args = ["grim"]
+ elif shutil.which("spectacle"):
+ args = ["spectacle", "-n", "-b", "-f", "-o"]
+ else:
+ raise
fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
- subprocess.call(["gnome-screenshot", "-f", filepath])
+ subprocess.call(args + [filepath])
im = Image.open(filepath)
im.load()
os.unlink(filepath)
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 75dfbee22..da28854b5 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -213,14 +213,14 @@ def colorize(
blue = []
# Create the low-end values
- for i in range(0, blackpoint):
+ for i in range(blackpoint):
red.append(rgb_black[0])
green.append(rgb_black[1])
blue.append(rgb_black[2])
# Create the mapping (2-color)
if rgb_mid is None:
- range_map = range(0, whitepoint - blackpoint)
+ range_map = range(whitepoint - blackpoint)
for i in range_map:
red.append(
@@ -235,8 +235,8 @@ def colorize(
# Create the mapping (3-color)
else:
- range_map1 = range(0, midpoint - blackpoint)
- range_map2 = range(0, whitepoint - midpoint)
+ range_map1 = range(midpoint - blackpoint)
+ range_map2 = range(whitepoint - midpoint)
for i in range_map1:
red.append(
@@ -256,7 +256,7 @@ def colorize(
blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
# Create the high-end values
- for i in range(0, 256 - whitepoint):
+ for i in range(256 - whitepoint):
red.append(rgb_white[0])
green.append(rgb_white[1])
blue.append(rgb_white[2])
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index 183f85526..103697117 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -19,10 +19,11 @@ from __future__ import annotations
import array
from collections.abc import Sequence
-from typing import IO, TYPE_CHECKING
+from typing import IO
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
+TYPE_CHECKING = False
if TYPE_CHECKING:
from . import Image
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index 2cc40f855..df7a57b65 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -19,11 +19,12 @@ from __future__ import annotations
import sys
from io import BytesIO
-from typing import TYPE_CHECKING, Any, Callable, Union
+from typing import Any, Callable, Union
from . import Image
from ._util import is_path
+TYPE_CHECKING = False
if TYPE_CHECKING:
import PyQt6
import PySide6
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index d62893d9c..dd240fb55 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -192,7 +192,7 @@ if sys.platform == "darwin":
register(MacViewer)
-class UnixViewer(Viewer):
+class UnixViewer(abc.ABC, Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py
index e6a9d8eea..3a4cb81e9 100644
--- a/src/PIL/ImageTk.py
+++ b/src/PIL/ImageTk.py
@@ -28,10 +28,11 @@ from __future__ import annotations
import tkinter
from io import BytesIO
-from typing import TYPE_CHECKING, Any
+from typing import Any
from . import Image, ImageFile
+TYPE_CHECKING = False
if TYPE_CHECKING:
from ._typing import CapsuleType
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 3e882403b..969528841 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -42,7 +42,7 @@ import subprocess
import sys
import tempfile
import warnings
-from typing import IO, TYPE_CHECKING, Any
+from typing import IO, Any
from . import Image, ImageFile
from ._binary import i16be as i16
@@ -52,6 +52,7 @@ from ._binary import o16be as o16
from ._deprecate import deprecate
from .JpegPresets import presets
+TYPE_CHECKING = False
if TYPE_CHECKING:
from .MpoImagePlugin import MpoImageFile
@@ -402,8 +403,8 @@ class JpegImageFile(ImageFile.ImageFile):
return super().__getstate__() + [self.layers, self.layer]
def __setstate__(self, state: list[Any]) -> None:
+ self.layers, self.layer = state[6:]
super().__setstate__(state)
- self.layers, self.layer = state[5:]
def load_read(self, read_bytes: int) -> bytes:
"""
@@ -569,7 +570,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
mpentries = []
try:
rawmpentries = mp[0xB002]
- for entrynum in range(0, quant):
+ for entrynum in range(quant):
unpackedentry = struct.unpack_from(
f"{endianness}LLLHH", rawmpentries, entrynum * 16
)
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index e08f80b6b..f7393eac0 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -32,6 +32,7 @@ from . import (
TiffImagePlugin,
)
from ._binary import o32le
+from ._util import DeferredError
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
@@ -125,11 +126,15 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self.readonly = 1
def load_seek(self, pos: int) -> None:
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
self._fp.seek(pos)
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
self.fp = self._fp
self.offset = self.__mpoffsets[frame]
diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py
index 02939d26b..7fd4c5c94 100644
--- a/src/PIL/PSDraw.py
+++ b/src/PIL/PSDraw.py
@@ -17,10 +17,13 @@
from __future__ import annotations
import sys
-from typing import IO, TYPE_CHECKING
+from typing import IO
from . import EpsImagePlugin
+TYPE_CHECKING = False
+
+
##
# Simple PostScript graphics interface.
diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py
index b33245376..15f712908 100644
--- a/src/PIL/PalmImagePlugin.py
+++ b/src/PIL/PalmImagePlugin.py
@@ -116,9 +116,6 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00}
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "P":
- # we assume this is a color Palm image with the standard colormap,
- # unless the "info" dict has a "custom-colormap" field
-
rawmode = "P"
bpp = 8
version = 1
@@ -172,12 +169,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
compression_type = _COMPRESSION_TYPES["none"]
flags = 0
- if im.mode == "P" and "custom-colormap" in im.info:
- assert im.palette is not None
- flags = flags & _FLAGS["custom-colormap"]
- colormapsize = 4 * 256 + 2
- colormapmode = im.palette.mode
- colormap = im.getdata().getpalette()
+ if im.mode == "P":
+ flags |= _FLAGS["custom-colormap"]
+ colormap = im.im.getpalette()
+ colors = len(colormap) // 3
+ colormapsize = 4 * colors + 2
else:
colormapsize = 0
@@ -196,22 +192,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# now write colormap if necessary
- if colormapsize > 0:
- fp.write(o16b(256))
- for i in range(256):
+ if colormapsize:
+ fp.write(o16b(colors))
+ for i in range(colors):
fp.write(o8(i))
- if colormapmode == "RGB":
- fp.write(
- o8(colormap[3 * i])
- + o8(colormap[3 * i + 1])
- + o8(colormap[3 * i + 2])
- )
- elif colormapmode == "RGBA":
- fp.write(
- o8(colormap[4 * i])
- + o8(colormap[4 * i + 1])
- + o8(colormap[4 * i + 2])
- )
+ fp.write(colormap[3 * i : 3 * i + 3])
# now convert data to raw form
ImageFile._save(
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index 41b38ebbf..73d8c21c0 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -8,7 +8,7 @@ import os
import re
import time
import zlib
-from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union
+from typing import IO, Any, NamedTuple, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@@ -251,6 +251,7 @@ class PdfArray(list[Any]):
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
+TYPE_CHECKING = False
if TYPE_CHECKING:
_DictBase = collections.UserDict[Union[str, bytes], Any]
else:
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 4fc6217e1..f3815a122 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -40,7 +40,7 @@ import warnings
import zlib
from collections.abc import Callable
from enum import IntEnum
-from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast
+from typing import IO, Any, NamedTuple, NoReturn, cast
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16
@@ -48,7 +48,9 @@ from ._binary import i32be as i32
from ._binary import o8
from ._binary import o16be as o16
from ._binary import o32be as o32
+from ._util import DeferredError
+TYPE_CHECKING = False
if TYPE_CHECKING:
from . import _imaging
@@ -869,6 +871,8 @@ class PngImageFile(ImageFile.ImageFile):
def _seek(self, frame: int, rewind: bool = False) -> None:
assert self.png is not None
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
self.dispose: _imaging.ImagingCore | None
dispose_extent = None
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index 0aada8a06..f49aaeeb1 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -27,6 +27,7 @@ from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import si16be as si16
from ._binary import si32be as si32
+from ._util import DeferredError
MODES = {
# (photoshop mode, bits) -> (pil mode, required channels)
@@ -148,6 +149,8 @@ class PsdImageFile(ImageFile.ImageFile):
) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
layers = []
if self._layers_position is not None:
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
self._fp.seek(self._layers_position)
_layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size))
layers = _layerinfo(_layer_data, self._layers_size)
@@ -167,6 +170,8 @@ class PsdImageFile(ImageFile.ImageFile):
def seek(self, layer: int) -> None:
if not self._seek_check(layer):
return
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
# seek to given layer (1..max)
_, mode, _, tile = self.layers[layer - 1]
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index b26e1a996..868019e80 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -37,9 +37,12 @@ from __future__ import annotations
import os
import struct
import sys
-from typing import IO, TYPE_CHECKING, Any, cast
+from typing import IO, Any, cast
from . import Image, ImageFile
+from ._util import DeferredError
+
+TYPE_CHECKING = False
def isInt(f: Any) -> int:
@@ -178,6 +181,8 @@ class SpiderImageFile(ImageFile.ImageFile):
raise EOFError(msg)
if not self._seek_check(frame):
return
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
self.fp = self._fp
self.fp.seek(self.stkoffset)
diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py
index 779288b1c..86490a496 100644
--- a/src/PIL/TarIO.py
+++ b/src/PIL/TarIO.py
@@ -35,12 +35,16 @@ class TarIO(ContainerIO.ContainerIO[bytes]):
while True:
s = self.fh.read(512)
if len(s) != 512:
+ self.fh.close()
+
msg = "unexpected end of tar file"
raise OSError(msg)
name = s[:100].decode("utf-8")
i = name.find("\0")
if i == 0:
+ self.fh.close()
+
msg = "cannot find subfile"
raise OSError(msg)
if i > 0:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 3d36d1abc..88af9162e 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -50,7 +50,7 @@ import warnings
from collections.abc import Iterator, MutableMapping
from fractions import Fraction
from numbers import Number, Rational
-from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast
+from typing import IO, Any, Callable, NoReturn, cast
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
@@ -58,9 +58,10 @@ from ._binary import i32be as i32
from ._binary import o8
from ._deprecate import deprecate
from ._typing import StrOrBytesPath
-from ._util import is_path
+from ._util import DeferredError, is_path
from .TiffTags import TYPES
+TYPE_CHECKING = False
if TYPE_CHECKING:
from ._typing import Buffer, IntegralLike
@@ -1222,6 +1223,8 @@ class TiffImageFile(ImageFile.ImageFile):
self._im = None
def _seek(self, frame: int) -> None:
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
self.fp = self._fp
while len(self._frame_pos) <= frame:
@@ -1584,7 +1587,7 @@ class TiffImageFile(ImageFile.ImageFile):
# byte order.
elif rawmode == "I;16":
rawmode = "I;16N"
- elif rawmode.endswith(";16B") or rawmode.endswith(";16L"):
+ elif rawmode.endswith((";16B", ";16L")):
rawmode = rawmode[:-1] + "N"
# Offset in the tile tuple is 0, we go from 0,0 to
@@ -1608,6 +1611,10 @@ class TiffImageFile(ImageFile.ImageFile):
raise ValueError(msg)
w = tilewidth
+ if w == xsize and h == ysize and self._planar_configuration != 2:
+ # Every tile covers the image. Only use the last offset
+ offsets = offsets[-1:]
+
for offset in offsets:
if x + w > xsize:
stride = w * sum(bps_tuple) / 8 # bytes per line
@@ -1630,11 +1637,11 @@ class TiffImageFile(ImageFile.ImageFile):
args,
)
)
- x = x + w
+ x += w
if x >= xsize:
x, y = 0, y + h
if y >= ysize:
- x = y = 0
+ y = 0
layer += 1
else:
logger.debug("- unsupported data organization")
@@ -2295,9 +2302,7 @@ class AppendingTiffWriter(io.BytesIO):
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
- encoderinfo = im.encoderinfo.copy()
- encoderconfig = im.encoderconfig
- append_images = list(encoderinfo.get("append_images", []))
+ append_images = list(im.encoderinfo.get("append_images", []))
if not hasattr(im, "n_frames") and not append_images:
return _save(im, fp, filename)
@@ -2305,12 +2310,11 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try:
with AppendingTiffWriter(fp) as tf:
for ims in [im] + append_images:
- ims.encoderinfo = encoderinfo
- ims.encoderconfig = encoderconfig
- if not hasattr(ims, "n_frames"):
- nfr = 1
- else:
- nfr = ims.n_frames
+ if not hasattr(ims, "encoderinfo"):
+ ims.encoderinfo = {}
+ if not hasattr(ims, "encoderconfig"):
+ ims.encoderconfig = ()
+ nfr = getattr(ims, "n_frames", 1)
for idx in range(nfr):
ims.seek(idx)
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index c2dde4431..1716a18cc 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -238,7 +238,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
cur_idx = im.tell()
try:
for ims in [im] + append_images:
- # Get # of frames in this image
+ # Get number of frames in this image
nfr = getattr(ims, "n_frames", 1)
for idx in range(nfr):
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index 04abd52f0..f709d026b 100644
--- a/src/PIL/WmfImagePlugin.py
+++ b/src/PIL/WmfImagePlugin.py
@@ -80,8 +80,6 @@ class WmfStubImageFile(ImageFile.StubImageFile):
format_description = "Windows Metafile"
def _open(self) -> None:
- self._inch = None
-
# check placable header
s = self.fp.read(80)
@@ -89,10 +87,11 @@ class WmfStubImageFile(ImageFile.StubImageFile):
# placeable windows metafile
# get units per inch
- self._inch = word(s, 14)
- if self._inch == 0:
+ inch = word(s, 14)
+ if inch == 0:
msg = "Invalid inch"
raise ValueError(msg)
+ self._inch: tuple[float, float] = inch, inch
# get bounding box
x0 = short(s, 6)
@@ -103,8 +102,8 @@ class WmfStubImageFile(ImageFile.StubImageFile):
# normalize size to 72 dots per inch
self.info["dpi"] = 72
size = (
- (x1 - x0) * self.info["dpi"] // self._inch,
- (y1 - y0) * self.info["dpi"] // self._inch,
+ (x1 - x0) * self.info["dpi"] // inch,
+ (y1 - y0) * self.info["dpi"] // inch,
)
self.info["wmf_bbox"] = x0, y0, x1, y1
@@ -138,6 +137,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
self.info["dpi"] = xdpi
else:
self.info["dpi"] = xdpi, ydpi
+ self._inch = xdpi, ydpi
else:
msg = "Unsupported file format"
@@ -153,13 +153,17 @@ class WmfStubImageFile(ImageFile.StubImageFile):
def _load(self) -> ImageFile.StubHandler | None:
return _handler
- def load(self, dpi: int | None = None) -> Image.core.PixelAccess | None:
- if dpi is not None and self._inch is not None:
+ def load(
+ self, dpi: float | tuple[float, float] | None = None
+ ) -> Image.core.PixelAccess | None:
+ if dpi is not None:
self.info["dpi"] = dpi
x0, y0, x1, y1 = self.info["wmf_bbox"]
+ if not isinstance(dpi, tuple):
+ dpi = dpi, dpi
self._size = (
- (x1 - x0) * self.info["dpi"] // self._inch,
- (y1 - y0) * self.info["dpi"] // self._inch,
+ int((x1 - x0) * dpi[0] / self._inch[0]),
+ int((y1 - y0) * dpi[1] / self._inch[1]),
)
return super().load()
diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py
index b47e7e365..fff812234 100644
--- a/src/PIL/__init__.py
+++ b/src/PIL/__init__.py
@@ -26,6 +26,7 @@ del _version
_plugins = [
"AniImagePlugin",
+ "AvifImagePlugin",
"BlpImagePlugin",
"BmpImagePlugin",
"BufrStubImagePlugin",
diff --git a/src/PIL/_avif.pyi b/src/PIL/_avif.pyi
new file mode 100644
index 000000000..e27843e53
--- /dev/null
+++ b/src/PIL/_avif.pyi
@@ -0,0 +1,3 @@
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py
index 34a9a81e1..373938e71 100644
--- a/src/PIL/_typing.py
+++ b/src/PIL/_typing.py
@@ -3,8 +3,9 @@ from __future__ import annotations
import os
import sys
from collections.abc import Sequence
-from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union
+from typing import Any, Protocol, TypeVar, Union
+TYPE_CHECKING = False
if TYPE_CHECKING:
from numbers import _IntegralLike as IntegralLike
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index e93c7887b..ac678c7d2 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
-__version__ = "11.2.0.dev0"
+__version__ = "11.3.0.dev0"
diff --git a/src/PIL/features.py b/src/PIL/features.py
index ae7ea4255..573f1d412 100644
--- a/src/PIL/features.py
+++ b/src/PIL/features.py
@@ -17,6 +17,7 @@ modules = {
"freetype2": ("PIL._imagingft", "freetype2_version"),
"littlecms2": ("PIL._imagingcms", "littlecms_version"),
"webp": ("PIL._webp", "webpdecoder_version"),
+ "avif": ("PIL._avif", "libavif_version"),
}
@@ -288,6 +289,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
("freetype2", "FREETYPE2"),
("littlecms2", "LITTLECMS2"),
("webp", "WEBP"),
+ ("avif", "AVIF"),
("jpg", "JPEG"),
("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"),
diff --git a/src/_avif.c b/src/_avif.c
new file mode 100644
index 000000000..7e7bee703
--- /dev/null
+++ b/src/_avif.c
@@ -0,0 +1,906 @@
+#define PY_SSIZE_T_CLEAN
+
+#include
+#include "avif/avif.h"
+
+// Encoder type
+typedef struct {
+ PyObject_HEAD avifEncoder *encoder;
+ avifImage *image;
+ int first_frame;
+} AvifEncoderObject;
+
+static PyTypeObject AvifEncoder_Type;
+
+// Decoder type
+typedef struct {
+ PyObject_HEAD avifDecoder *decoder;
+ Py_buffer buffer;
+} AvifDecoderObject;
+
+static PyTypeObject AvifDecoder_Type;
+
+static int
+normalize_tiles_log2(int value) {
+ if (value < 0) {
+ return 0;
+ } else if (value > 6) {
+ return 6;
+ } else {
+ return value;
+ }
+}
+
+static PyObject *
+exc_type_for_avif_result(avifResult result) {
+ switch (result) {
+ case AVIF_RESULT_INVALID_EXIF_PAYLOAD:
+ case AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION:
+ return PyExc_ValueError;
+ case AVIF_RESULT_INVALID_FTYP:
+ case AVIF_RESULT_BMFF_PARSE_FAILED:
+ case AVIF_RESULT_TRUNCATED_DATA:
+ case AVIF_RESULT_NO_CONTENT:
+ return PyExc_SyntaxError;
+ default:
+ return PyExc_RuntimeError;
+ }
+}
+
+static uint8_t
+irot_imir_to_exif_orientation(const avifImage *image) {
+ uint8_t axis = image->imir.axis;
+ int imir = image->transformFlags & AVIF_TRANSFORM_IMIR;
+ int irot = image->transformFlags & AVIF_TRANSFORM_IROT;
+ if (irot) {
+ uint8_t angle = image->irot.angle;
+ if (angle == 1) {
+ if (imir) {
+ return axis ? 7 // 90 degrees anti-clockwise then swap left and right.
+ : 5; // 90 degrees anti-clockwise then swap top and bottom.
+ }
+ return 8; // 90 degrees anti-clockwise.
+ }
+ if (angle == 2) {
+ if (imir) {
+ return axis
+ ? 4 // 180 degrees anti-clockwise then swap left and right.
+ : 2; // 180 degrees anti-clockwise then swap top and bottom.
+ }
+ return 3; // 180 degrees anti-clockwise.
+ }
+ if (angle == 3) {
+ if (imir) {
+ return axis
+ ? 5 // 270 degrees anti-clockwise then swap left and right.
+ : 7; // 270 degrees anti-clockwise then swap top and bottom.
+ }
+ return 6; // 270 degrees anti-clockwise.
+ }
+ }
+ if (imir) {
+ return axis ? 2 // Swap left and right.
+ : 4; // Swap top and bottom.
+ }
+ return 1; // Default orientation ("top-left", no-op).
+}
+
+static void
+exif_orientation_to_irot_imir(avifImage *image, int orientation) {
+ // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A
+ // Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021
+ // sections 6.5.10 and 6.5.12.
+ switch (orientation) {
+ case 2: // The 0th row is at the visual top of the image, and the 0th column is
+ // the visual right-hand side.
+ image->transformFlags |= AVIF_TRANSFORM_IMIR;
+ image->imir.axis = 1;
+ break;
+ case 3: // The 0th row is at the visual bottom of the image, and the 0th column
+ // is the visual right-hand side.
+ image->transformFlags |= AVIF_TRANSFORM_IROT;
+ image->irot.angle = 2;
+ break;
+ case 4: // The 0th row is at the visual bottom of the image, and the 0th column
+ // is the visual left-hand side.
+ image->transformFlags |= AVIF_TRANSFORM_IMIR;
+ break;
+ case 5: // The 0th row is the visual left-hand side of the image, and the 0th
+ // column is the visual top.
+ image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
+ image->irot.angle = 1; // applied before imir according to MIAF spec
+ // ISO/IEC 28002-12:2021 - section 7.3.6.7
+ break;
+ case 6: // The 0th row is the visual right-hand side of the image, and the 0th
+ // column is the visual top.
+ image->transformFlags |= AVIF_TRANSFORM_IROT;
+ image->irot.angle = 3;
+ break;
+ case 7: // The 0th row is the visual right-hand side of the image, and the 0th
+ // column is the visual bottom.
+ image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
+ image->irot.angle = 3; // applied before imir according to MIAF spec
+ // ISO/IEC 28002-12:2021 - section 7.3.6.7
+ break;
+ case 8: // The 0th row is the visual left-hand side of the image, and the 0th
+ // column is the visual bottom.
+ image->transformFlags |= AVIF_TRANSFORM_IROT;
+ image->irot.angle = 1;
+ break;
+ }
+}
+
+static int
+_codec_available(const char *name, avifCodecFlags flags) {
+ avifCodecChoice codec = avifCodecChoiceFromName(name);
+ if (codec == AVIF_CODEC_CHOICE_AUTO) {
+ return 0;
+ }
+ const char *codec_name = avifCodecName(codec, flags);
+ return (codec_name == NULL) ? 0 : 1;
+}
+
+PyObject *
+_decoder_codec_available(PyObject *self, PyObject *args) {
+ char *codec_name;
+ if (!PyArg_ParseTuple(args, "s", &codec_name)) {
+ return NULL;
+ }
+ int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_DECODE);
+ return PyBool_FromLong(is_available);
+}
+
+PyObject *
+_encoder_codec_available(PyObject *self, PyObject *args) {
+ char *codec_name;
+ if (!PyArg_ParseTuple(args, "s", &codec_name)) {
+ return NULL;
+ }
+ int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_ENCODE);
+ return PyBool_FromLong(is_available);
+}
+
+PyObject *
+_codec_versions(PyObject *self, PyObject *args) {
+ char buffer[256];
+ avifCodecVersions(buffer);
+ return PyUnicode_FromString(buffer);
+}
+
+static int
+_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) {
+ Py_ssize_t i, size;
+ PyObject *keyval, *py_key, *py_val;
+ if (!PyTuple_Check(opts)) {
+ PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
+ return 1;
+ }
+ size = PyTuple_GET_SIZE(opts);
+
+ for (i = 0; i < size; i++) {
+ keyval = PyTuple_GetItem(opts, i);
+ if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) {
+ PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
+ return 1;
+ }
+ py_key = PyTuple_GetItem(keyval, 0);
+ py_val = PyTuple_GetItem(keyval, 1);
+ if (!PyUnicode_Check(py_key) || !PyUnicode_Check(py_val)) {
+ PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
+ return 1;
+ }
+ const char *key = PyUnicode_AsUTF8(py_key);
+ const char *val = PyUnicode_AsUTF8(py_val);
+ if (key == NULL || val == NULL) {
+ PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
+ return 1;
+ }
+
+ avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val);
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Setting advanced codec options failed: %s",
+ avifResultToString(result)
+ );
+ return 1;
+ }
+ }
+ return 0;
+}
+
+// Encoder functions
+PyObject *
+AvifEncoderNew(PyObject *self_, PyObject *args) {
+ unsigned int width, height;
+ AvifEncoderObject *self = NULL;
+ avifEncoder *encoder = NULL;
+
+ char *subsampling;
+ int quality;
+ int speed;
+ int exif_orientation;
+ int max_threads;
+ Py_buffer icc_buffer;
+ Py_buffer exif_buffer;
+ Py_buffer xmp_buffer;
+ int alpha_premultiplied;
+ int autotiling;
+ int tile_rows_log2;
+ int tile_cols_log2;
+
+ char *codec;
+ char *range;
+
+ PyObject *advanced;
+ int error = 0;
+
+ if (!PyArg_ParseTuple(
+ args,
+ "(II)siiissiippy*y*iy*O",
+ &width,
+ &height,
+ &subsampling,
+ &quality,
+ &speed,
+ &max_threads,
+ &codec,
+ &range,
+ &tile_rows_log2,
+ &tile_cols_log2,
+ &alpha_premultiplied,
+ &autotiling,
+ &icc_buffer,
+ &exif_buffer,
+ &exif_orientation,
+ &xmp_buffer,
+ &advanced
+ )) {
+ return NULL;
+ }
+
+ // Create a new animation encoder and picture frame
+ avifImage *image = avifImageCreateEmpty();
+ if (image == NULL) {
+ PyErr_SetString(PyExc_ValueError, "Image creation failed");
+ error = 1;
+ goto end;
+ }
+
+ // Set these in advance so any upcoming RGB -> YUV use the proper coefficients
+ if (strcmp(range, "full") == 0) {
+ image->yuvRange = AVIF_RANGE_FULL;
+ } else if (strcmp(range, "limited") == 0) {
+ image->yuvRange = AVIF_RANGE_LIMITED;
+ } else {
+ PyErr_SetString(PyExc_ValueError, "Invalid range");
+ error = 1;
+ goto end;
+ }
+ if (strcmp(subsampling, "4:0:0") == 0) {
+ image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400;
+ } else if (strcmp(subsampling, "4:2:0") == 0) {
+ image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420;
+ } else if (strcmp(subsampling, "4:2:2") == 0) {
+ image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422;
+ } else if (strcmp(subsampling, "4:4:4") == 0) {
+ image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444;
+ } else {
+ PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling);
+ error = 1;
+ goto end;
+ }
+
+ // Validate canvas dimensions
+ if (width == 0 || height == 0) {
+ PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions");
+ error = 1;
+ goto end;
+ }
+ image->width = width;
+ image->height = height;
+
+ image->depth = 8;
+ image->alphaPremultiplied = alpha_premultiplied ? AVIF_TRUE : AVIF_FALSE;
+
+ encoder = avifEncoderCreate();
+ if (!encoder) {
+ PyErr_SetString(PyExc_MemoryError, "Can't allocate encoder");
+ error = 1;
+ goto end;
+ }
+
+ int is_aom_encode = strcmp(codec, "aom") == 0 ||
+ (strcmp(codec, "auto") == 0 &&
+ _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE));
+ encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads;
+
+ encoder->quality = quality;
+
+ if (strcmp(codec, "auto") == 0) {
+ encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO;
+ } else {
+ encoder->codecChoice = avifCodecChoiceFromName(codec);
+ }
+ if (speed < AVIF_SPEED_SLOWEST) {
+ speed = AVIF_SPEED_SLOWEST;
+ } else if (speed > AVIF_SPEED_FASTEST) {
+ speed = AVIF_SPEED_FASTEST;
+ }
+ encoder->speed = speed;
+ encoder->timescale = (uint64_t)1000;
+
+ encoder->autoTiling = autotiling ? AVIF_TRUE : AVIF_FALSE;
+ if (!autotiling) {
+ encoder->tileRowsLog2 = normalize_tiles_log2(tile_rows_log2);
+ encoder->tileColsLog2 = normalize_tiles_log2(tile_cols_log2);
+ }
+
+ if (advanced != Py_None && _add_codec_specific_options(encoder, advanced)) {
+ error = 1;
+ goto end;
+ }
+
+ self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type);
+ if (!self) {
+ PyErr_SetString(PyExc_RuntimeError, "could not create encoder object");
+ error = 1;
+ goto end;
+ }
+ self->first_frame = 1;
+
+ avifResult result;
+ if (icc_buffer.len) {
+ result = avifImageSetProfileICC(image, icc_buffer.buf, icc_buffer.len);
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Setting ICC profile failed: %s",
+ avifResultToString(result)
+ );
+ error = 1;
+ goto end;
+ }
+ // colorPrimaries and transferCharacteristics are ignored when an ICC
+ // profile is present, so set them to UNSPECIFIED.
+ image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED;
+ image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED;
+ } else {
+ image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709;
+ image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
+ }
+ image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;
+
+ if (exif_buffer.len) {
+ result = avifImageSetMetadataExif(image, exif_buffer.buf, exif_buffer.len);
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Setting EXIF data failed: %s",
+ avifResultToString(result)
+ );
+ error = 1;
+ goto end;
+ }
+ }
+
+ if (xmp_buffer.len) {
+ result = avifImageSetMetadataXMP(image, xmp_buffer.buf, xmp_buffer.len);
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Setting XMP data failed: %s",
+ avifResultToString(result)
+ );
+ error = 1;
+ goto end;
+ }
+ }
+
+ if (exif_orientation > 1) {
+ exif_orientation_to_irot_imir(image, exif_orientation);
+ }
+
+ self->image = image;
+ self->encoder = encoder;
+
+end:
+ PyBuffer_Release(&icc_buffer);
+ PyBuffer_Release(&exif_buffer);
+ PyBuffer_Release(&xmp_buffer);
+
+ if (error) {
+ if (image) {
+ avifImageDestroy(image);
+ }
+ if (encoder) {
+ avifEncoderDestroy(encoder);
+ }
+ if (self) {
+ PyObject_Del(self);
+ }
+ return NULL;
+ }
+
+ return (PyObject *)self;
+}
+
+PyObject *
+_encoder_dealloc(AvifEncoderObject *self) {
+ if (self->encoder) {
+ avifEncoderDestroy(self->encoder);
+ }
+ if (self->image) {
+ avifImageDestroy(self->image);
+ }
+ Py_RETURN_NONE;
+}
+
+PyObject *
+_encoder_add(AvifEncoderObject *self, PyObject *args) {
+ uint8_t *rgb_bytes;
+ Py_ssize_t size;
+ unsigned int duration;
+ unsigned int width;
+ unsigned int height;
+ char *mode;
+ unsigned int is_single_frame;
+ int error = 0;
+
+ avifRGBImage rgb;
+ avifResult result;
+
+ avifEncoder *encoder = self->encoder;
+ avifImage *image = self->image;
+ avifImage *frame = NULL;
+
+ if (!PyArg_ParseTuple(
+ args,
+ "y#I(II)sp",
+ (char **)&rgb_bytes,
+ &size,
+ &duration,
+ &width,
+ &height,
+ &mode,
+ &is_single_frame
+ )) {
+ return NULL;
+ }
+
+ if (image->width != width || image->height != height) {
+ PyErr_Format(
+ PyExc_ValueError,
+ "Image sequence dimensions mismatch, %ux%u != %ux%u",
+ image->width,
+ image->height,
+ width,
+ height
+ );
+ return NULL;
+ }
+
+ if (self->first_frame) {
+ // If we don't have an image populated with yuv planes, this is the first frame
+ frame = image;
+ } else {
+ frame = avifImageCreateEmpty();
+ if (image == NULL) {
+ PyErr_SetString(PyExc_ValueError, "Image creation failed");
+ return NULL;
+ }
+
+ frame->width = width;
+ frame->height = height;
+ frame->colorPrimaries = image->colorPrimaries;
+ frame->transferCharacteristics = image->transferCharacteristics;
+ frame->matrixCoefficients = image->matrixCoefficients;
+ frame->yuvRange = image->yuvRange;
+ frame->yuvFormat = image->yuvFormat;
+ frame->depth = image->depth;
+ frame->alphaPremultiplied = image->alphaPremultiplied;
+ }
+
+ avifRGBImageSetDefaults(&rgb, frame);
+
+ if (strcmp(mode, "RGBA") == 0) {
+ rgb.format = AVIF_RGB_FORMAT_RGBA;
+ } else {
+ rgb.format = AVIF_RGB_FORMAT_RGB;
+ }
+
+ result = avifRGBImageAllocatePixels(&rgb);
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Pixel allocation failed: %s",
+ avifResultToString(result)
+ );
+ error = 1;
+ goto end;
+ }
+
+ if (rgb.rowBytes * rgb.height != size) {
+ PyErr_Format(
+ PyExc_RuntimeError,
+ "rgb data has incorrect size: %u * %u (%u) != %u",
+ rgb.rowBytes,
+ rgb.height,
+ rgb.rowBytes * rgb.height,
+ size
+ );
+ error = 1;
+ goto end;
+ }
+
+ // rgb.pixels is safe for writes
+ memcpy(rgb.pixels, rgb_bytes, size);
+
+ Py_BEGIN_ALLOW_THREADS;
+ result = avifImageRGBToYUV(frame, &rgb);
+ Py_END_ALLOW_THREADS;
+
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Conversion to YUV failed: %s",
+ avifResultToString(result)
+ );
+ error = 1;
+ goto end;
+ }
+
+ uint32_t addImageFlags =
+ is_single_frame ? AVIF_ADD_IMAGE_FLAG_SINGLE : AVIF_ADD_IMAGE_FLAG_NONE;
+
+ Py_BEGIN_ALLOW_THREADS;
+ result = avifEncoderAddImage(encoder, frame, duration, addImageFlags);
+ Py_END_ALLOW_THREADS;
+
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Failed to encode image: %s",
+ avifResultToString(result)
+ );
+ error = 1;
+ goto end;
+ }
+
+end:
+ avifRGBImageFreePixels(&rgb);
+ if (!self->first_frame) {
+ avifImageDestroy(frame);
+ }
+
+ if (error) {
+ return NULL;
+ }
+ self->first_frame = 0;
+ Py_RETURN_NONE;
+}
+
+PyObject *
+_encoder_finish(AvifEncoderObject *self) {
+ avifEncoder *encoder = self->encoder;
+
+ avifRWData raw = AVIF_DATA_EMPTY;
+ avifResult result;
+ PyObject *ret = NULL;
+
+ Py_BEGIN_ALLOW_THREADS;
+ result = avifEncoderFinish(encoder, &raw);
+ Py_END_ALLOW_THREADS;
+
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Failed to finish encoding: %s",
+ avifResultToString(result)
+ );
+ avifRWDataFree(&raw);
+ return NULL;
+ }
+
+ ret = PyBytes_FromStringAndSize((char *)raw.data, raw.size);
+
+ avifRWDataFree(&raw);
+
+ return ret;
+}
+
+// Decoder functions
+PyObject *
+AvifDecoderNew(PyObject *self_, PyObject *args) {
+ Py_buffer buffer;
+ AvifDecoderObject *self = NULL;
+ avifDecoder *decoder;
+
+ char *codec_str;
+ avifCodecChoice codec;
+ int max_threads;
+
+ avifResult result;
+
+ if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) {
+ return NULL;
+ }
+
+ if (strcmp(codec_str, "auto") == 0) {
+ codec = AVIF_CODEC_CHOICE_AUTO;
+ } else {
+ codec = avifCodecChoiceFromName(codec_str);
+ }
+
+ self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type);
+ if (!self) {
+ PyErr_SetString(PyExc_RuntimeError, "could not create decoder object");
+ PyBuffer_Release(&buffer);
+ return NULL;
+ }
+
+ decoder = avifDecoderCreate();
+ if (!decoder) {
+ PyErr_SetString(PyExc_MemoryError, "Can't allocate decoder");
+ PyBuffer_Release(&buffer);
+ PyObject_Del(self);
+ return NULL;
+ }
+ decoder->maxThreads = max_threads;
+ // Turn off libavif's 'clap' (clean aperture) property validation.
+ decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID;
+ // Allow the PixelInformationProperty ('pixi') to be missing in AV1 image
+ // items. libheif v1.11.0 and older does not add the 'pixi' item property to
+ // AV1 image items.
+ decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED;
+ decoder->codecChoice = codec;
+
+ result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len);
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Setting IO memory failed: %s",
+ avifResultToString(result)
+ );
+ avifDecoderDestroy(decoder);
+ PyBuffer_Release(&buffer);
+ PyObject_Del(self);
+ return NULL;
+ }
+
+ result = avifDecoderParse(decoder);
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Failed to decode image: %s",
+ avifResultToString(result)
+ );
+ avifDecoderDestroy(decoder);
+ PyBuffer_Release(&buffer);
+ PyObject_Del(self);
+ return NULL;
+ }
+
+ self->decoder = decoder;
+ self->buffer = buffer;
+
+ return (PyObject *)self;
+}
+
+PyObject *
+_decoder_dealloc(AvifDecoderObject *self) {
+ if (self->decoder) {
+ avifDecoderDestroy(self->decoder);
+ }
+ PyBuffer_Release(&self->buffer);
+ Py_RETURN_NONE;
+}
+
+PyObject *
+_decoder_get_info(AvifDecoderObject *self) {
+ avifDecoder *decoder = self->decoder;
+ avifImage *image = decoder->image;
+
+ PyObject *icc = NULL;
+ PyObject *exif = NULL;
+ PyObject *xmp = NULL;
+ PyObject *ret = NULL;
+
+ if (image->xmp.size) {
+ xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size);
+ }
+
+ if (image->exif.size) {
+ exif =
+ PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size);
+ }
+
+ if (image->icc.size) {
+ icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size);
+ }
+
+ ret = Py_BuildValue(
+ "(II)IsSSIS",
+ image->width,
+ image->height,
+ decoder->imageCount,
+ decoder->alphaPresent ? "RGBA" : "RGB",
+ NULL == icc ? Py_None : icc,
+ NULL == exif ? Py_None : exif,
+ irot_imir_to_exif_orientation(image),
+ NULL == xmp ? Py_None : xmp
+ );
+
+ Py_XDECREF(xmp);
+ Py_XDECREF(exif);
+ Py_XDECREF(icc);
+
+ return ret;
+}
+
+PyObject *
+_decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
+ PyObject *bytes;
+ PyObject *ret;
+ Py_ssize_t size;
+ avifResult result;
+ avifRGBImage rgb;
+ avifDecoder *decoder;
+ avifImage *image;
+ uint32_t frame_index;
+
+ decoder = self->decoder;
+
+ if (!PyArg_ParseTuple(args, "I", &frame_index)) {
+ return NULL;
+ }
+
+ result = avifDecoderNthImage(decoder, frame_index);
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Failed to decode frame %u: %s",
+ frame_index,
+ avifResultToString(result)
+ );
+ return NULL;
+ }
+
+ image = decoder->image;
+
+ avifRGBImageSetDefaults(&rgb, image);
+
+ rgb.depth = 8;
+ rgb.format = decoder->alphaPresent ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB;
+
+ result = avifRGBImageAllocatePixels(&rgb);
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Pixel allocation failed: %s",
+ avifResultToString(result)
+ );
+ return NULL;
+ }
+
+ Py_BEGIN_ALLOW_THREADS;
+ result = avifImageYUVToRGB(image, &rgb);
+ Py_END_ALLOW_THREADS;
+
+ if (result != AVIF_RESULT_OK) {
+ PyErr_Format(
+ exc_type_for_avif_result(result),
+ "Conversion from YUV failed: %s",
+ avifResultToString(result)
+ );
+ avifRGBImageFreePixels(&rgb);
+ return NULL;
+ }
+
+ if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) {
+ PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size");
+ return NULL;
+ }
+
+ size = rgb.rowBytes * rgb.height;
+
+ bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size);
+ avifRGBImageFreePixels(&rgb);
+
+ ret = Py_BuildValue(
+ "SKKK",
+ bytes,
+ decoder->timescale,
+ decoder->imageTiming.ptsInTimescales,
+ decoder->imageTiming.durationInTimescales
+ );
+
+ Py_DECREF(bytes);
+
+ return ret;
+}
+
+/* -------------------------------------------------------------------- */
+/* Type Definitions */
+/* -------------------------------------------------------------------- */
+
+// AvifEncoder methods
+static struct PyMethodDef _encoder_methods[] = {
+ {"add", (PyCFunction)_encoder_add, METH_VARARGS},
+ {"finish", (PyCFunction)_encoder_finish, METH_NOARGS},
+ {NULL, NULL} /* sentinel */
+};
+
+// AvifEncoder type definition
+static PyTypeObject AvifEncoder_Type = {
+ PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder",
+ .tp_basicsize = sizeof(AvifEncoderObject),
+ .tp_dealloc = (destructor)_encoder_dealloc,
+ .tp_methods = _encoder_methods,
+};
+
+// AvifDecoder methods
+static struct PyMethodDef _decoder_methods[] = {
+ {"get_info", (PyCFunction)_decoder_get_info, METH_NOARGS},
+ {"get_frame", (PyCFunction)_decoder_get_frame, METH_VARARGS},
+ {NULL, NULL} /* sentinel */
+};
+
+// AvifDecoder type definition
+static PyTypeObject AvifDecoder_Type = {
+ PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifDecoder",
+ .tp_basicsize = sizeof(AvifDecoderObject),
+ .tp_dealloc = (destructor)_decoder_dealloc,
+ .tp_methods = _decoder_methods,
+};
+
+/* -------------------------------------------------------------------- */
+/* Module Setup */
+/* -------------------------------------------------------------------- */
+
+static PyMethodDef avifMethods[] = {
+ {"AvifDecoder", AvifDecoderNew, METH_VARARGS},
+ {"AvifEncoder", AvifEncoderNew, METH_VARARGS},
+ {"decoder_codec_available", _decoder_codec_available, METH_VARARGS},
+ {"encoder_codec_available", _encoder_codec_available, METH_VARARGS},
+ {"codec_versions", _codec_versions, METH_NOARGS},
+ {NULL, NULL}
+};
+
+static int
+setup_module(PyObject *m) {
+ if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) {
+ return -1;
+ }
+
+ PyObject *d = PyModule_GetDict(m);
+ PyObject *v = PyUnicode_FromString(avifVersion());
+ PyDict_SetItemString(d, "libavif_version", v ? v : Py_None);
+ Py_XDECREF(v);
+
+ return 0;
+}
+
+PyMODINIT_FUNC
+PyInit__avif(void) {
+ PyObject *m;
+
+ static PyModuleDef module_def = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "_avif",
+ .m_size = -1,
+ .m_methods = avifMethods,
+ };
+
+ m = PyModule_Create(&module_def);
+ if (setup_module(m) < 0) {
+ Py_DECREF(m);
+ return NULL;
+ }
+
+#ifdef Py_GIL_DISABLED
+ PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
+#endif
+
+ return m;
+}
diff --git a/src/_imaging.c b/src/_imaging.c
index fa38dcc05..72f122143 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -230,6 +230,93 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) {
return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE);
}
+/* -------------------------------------------------------------------- */
+/* Arrow HANDLING */
+/* -------------------------------------------------------------------- */
+
+PyObject *
+ArrowError(int err) {
+ if (err == IMAGING_CODEC_MEMORY) {
+ return ImagingError_MemoryError();
+ }
+ if (err == IMAGING_ARROW_INCOMPATIBLE_MODE) {
+ return ImagingError_ValueError("Incompatible Pillow mode for Arrow array");
+ }
+ if (err == IMAGING_ARROW_MEMORY_LAYOUT) {
+ return ImagingError_ValueError(
+ "Image is in multiple array blocks, use imaging_new_block for zero copy"
+ );
+ }
+ return ImagingError_ValueError("Unknown error");
+}
+
+void
+ReleaseArrowSchemaPyCapsule(PyObject *capsule) {
+ struct ArrowSchema *schema =
+ (struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema");
+ if (schema->release != NULL) {
+ schema->release(schema);
+ }
+ free(schema);
+}
+
+PyObject *
+ExportArrowSchemaPyCapsule(ImagingObject *self) {
+ struct ArrowSchema *schema =
+ (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema));
+ int err = export_imaging_schema(self->image, schema);
+ if (err == 0) {
+ return PyCapsule_New(schema, "arrow_schema", ReleaseArrowSchemaPyCapsule);
+ }
+ free(schema);
+ return ArrowError(err);
+}
+
+void
+ReleaseArrowArrayPyCapsule(PyObject *capsule) {
+ struct ArrowArray *array =
+ (struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array");
+ if (array->release != NULL) {
+ array->release(array);
+ }
+ free(array);
+}
+
+PyObject *
+ExportArrowArrayPyCapsule(ImagingObject *self) {
+ struct ArrowArray *array =
+ (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray));
+ int err = export_imaging_array(self->image, array);
+ if (err == 0) {
+ return PyCapsule_New(array, "arrow_array", ReleaseArrowArrayPyCapsule);
+ }
+ free(array);
+ return ArrowError(err);
+}
+
+static PyObject *
+_new_arrow(PyObject *self, PyObject *args) {
+ char *mode;
+ int xsize, ysize;
+ PyObject *schema_capsule, *array_capsule;
+ PyObject *ret;
+
+ if (!PyArg_ParseTuple(
+ args, "s(ii)OO", &mode, &xsize, &ysize, &schema_capsule, &array_capsule
+ )) {
+ return NULL;
+ }
+
+ // ImagingBorrowArrow is responsible for retaining the array_capsule
+ ret =
+ PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule)
+ );
+ if (!ret) {
+ return ImagingError_ValueError("Invalid Arrow array mode or size mismatch");
+ }
+ return ret;
+}
+
/* -------------------------------------------------------------------- */
/* EXCEPTION REROUTING */
/* -------------------------------------------------------------------- */
@@ -3655,6 +3742,10 @@ static struct PyMethodDef methods[] = {
/* Misc. */
{"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS},
+ /* arrow */
+ {"__arrow_c_schema__", (PyCFunction)ExportArrowSchemaPyCapsule, METH_VARARGS},
+ {"__arrow_c_array__", (PyCFunction)ExportArrowArrayPyCapsule, METH_VARARGS},
+
{NULL, NULL} /* sentinel */
};
@@ -3722,6 +3813,11 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) {
);
}
+static PyObject *
+_getattr_readonly(ImagingObject *self, void *closure) {
+ return PyLong_FromLong(self->image->read_only);
+}
+
static struct PyGetSetDef getsetters[] = {
{"mode", (getter)_getattr_mode},
{"size", (getter)_getattr_size},
@@ -3729,6 +3825,7 @@ static struct PyGetSetDef getsetters[] = {
{"id", (getter)_getattr_id},
{"ptr", (getter)_getattr_ptr},
{"unsafe_ptrs", (getter)_getattr_unsafe_ptrs},
+ {"readonly", (getter)_getattr_readonly},
{NULL}
};
@@ -3983,6 +4080,21 @@ _set_blocks_max(PyObject *self, PyObject *args) {
Py_RETURN_NONE;
}
+static PyObject *
+_set_use_block_allocator(PyObject *self, PyObject *args) {
+ int use_block_allocator;
+ if (!PyArg_ParseTuple(args, "i:set_use_block_allocator", &use_block_allocator)) {
+ return NULL;
+ }
+ ImagingMemorySetBlockAllocator(&ImagingDefaultArena, use_block_allocator);
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+_get_use_block_allocator(PyObject *self, PyObject *args) {
+ return PyLong_FromLong(ImagingDefaultArena.use_block_allocator);
+}
+
static PyObject *
_clear_cache(PyObject *self, PyObject *args) {
int i = 0;
@@ -4041,6 +4153,8 @@ PyImaging_ZipDecoderNew(PyObject *self, PyObject *args);
/* Encoders (in encode.c) */
extern PyObject *
+PyImaging_BcnEncoderNew(PyObject *self, PyObject *args);
+extern PyObject *
PyImaging_EpsEncoderNew(PyObject *self, PyObject *args);
extern PyObject *
PyImaging_GifEncoderNew(PyObject *self, PyObject *args);
@@ -4102,6 +4216,7 @@ static PyMethodDef functions[] = {
{"fill", (PyCFunction)_fill, METH_VARARGS},
{"new", (PyCFunction)_new, METH_VARARGS},
{"new_block", (PyCFunction)_new_block, METH_VARARGS},
+ {"new_arrow", (PyCFunction)_new_arrow, METH_VARARGS},
{"merge", (PyCFunction)_merge, METH_VARARGS},
/* Functions */
@@ -4109,6 +4224,7 @@ static PyMethodDef functions[] = {
/* Codecs */
{"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS},
+ {"bcn_encoder", (PyCFunction)PyImaging_BcnEncoderNew, METH_VARARGS},
{"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS},
{"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS},
{"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS},
@@ -4187,9 +4303,11 @@ static PyMethodDef functions[] = {
{"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS},
{"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS},
{"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS},
+ {"get_use_block_allocator", (PyCFunction)_get_use_block_allocator, METH_VARARGS},
{"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS},
{"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS},
{"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS},
+ {"set_use_block_allocator", (PyCFunction)_set_use_block_allocator, METH_VARARGS},
{"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS},
{NULL, NULL} /* sentinel */
diff --git a/src/_imagingcms.c b/src/_imagingcms.c
index ea2f70186..f93c1613b 100644
--- a/src/_imagingcms.c
+++ b/src/_imagingcms.c
@@ -1402,8 +1402,8 @@ static struct PyGetSetDef cms_profile_getsetters[] = {
{"colorant_table_out", (getter)cms_profile_getattr_colorant_table_out},
{"intent_supported", (getter)cms_profile_getattr_is_intent_supported},
{"clut", (getter)cms_profile_getattr_is_clut},
- {"icc_measurement_condition", (getter)cms_profile_getattr_icc_measurement_condition
- },
+ {"icc_measurement_condition",
+ (getter)cms_profile_getattr_icc_measurement_condition},
{"icc_viewing_condition", (getter)cms_profile_getattr_icc_viewing_condition},
{NULL}
diff --git a/src/display.c b/src/display.c
index a05387504..11742a895 100644
--- a/src/display.c
+++ b/src/display.c
@@ -286,29 +286,42 @@ PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) {
/* -------------------------------------------------------------------- */
/* Windows screen grabber */
+typedef HANDLE(__stdcall *Func_GetWindowDpiAwarenessContext)(HANDLE);
typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE);
PyObject *
PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
- int x = 0, y = 0, width, height;
- int includeLayeredWindows = 0, all_screens = 0;
+ int x = 0, y = 0, width = -1, height;
+ int includeLayeredWindows = 0, screens = 0;
HBITMAP bitmap;
BITMAPCOREHEADER core;
HDC screen, screen_copy;
+ HWND wnd;
DWORD rop;
PyObject *buffer;
- HANDLE dpiAwareness;
+ HANDLE dpiAwareness = NULL;
HMODULE user32;
+ Func_GetWindowDpiAwarenessContext GetWindowDpiAwarenessContext_function;
Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function;
- if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) {
+ if (!PyArg_ParseTuple(
+ args, "|ii" F_HANDLE, &includeLayeredWindows, &screens, &wnd
+ )) {
return NULL;
}
/* step 1: create a memory DC large enough to hold the
entire screen */
- screen = CreateDC("DISPLAY", NULL, NULL, NULL);
+ if (screens == -1) {
+ screen = GetDC(wnd);
+ if (screen == NULL) {
+ PyErr_SetString(PyExc_OSError, "unable to get device context for handle");
+ return NULL;
+ }
+ } else {
+ screen = CreateDC("DISPLAY", NULL, NULL, NULL);
+ }
screen_copy = CreateCompatibleDC(screen);
// added in Windows 10 (1607)
@@ -317,15 +330,28 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext
)GetProcAddress(user32, "SetThreadDpiAwarenessContext");
if (SetThreadDpiAwarenessContext_function != NULL) {
+ GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext
+ )GetProcAddress(user32, "GetWindowDpiAwarenessContext");
+ if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) {
+ dpiAwareness = GetWindowDpiAwarenessContext_function(wnd);
+ }
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3)
- dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3);
+ dpiAwareness = SetThreadDpiAwarenessContext_function(
+ dpiAwareness == NULL ? (HANDLE)-3 : dpiAwareness
+ );
}
- if (all_screens) {
+ if (screens == 1) {
x = GetSystemMetrics(SM_XVIRTUALSCREEN);
y = GetSystemMetrics(SM_YVIRTUALSCREEN);
width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
+ } else if (screens == -1) {
+ RECT rect;
+ if (GetClientRect(wnd, &rect)) {
+ width = rect.right;
+ height = rect.bottom;
+ }
} else {
width = GetDeviceCaps(screen, HORZRES);
height = GetDeviceCaps(screen, VERTRES);
@@ -337,6 +363,10 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
FreeLibrary(user32);
+ if (width == -1) {
+ goto error;
+ }
+
bitmap = CreateCompatibleBitmap(screen, width, height);
if (!bitmap) {
goto error;
@@ -382,7 +412,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
DeleteObject(bitmap);
DeleteDC(screen_copy);
- DeleteDC(screen);
+ if (screens == -1) {
+ ReleaseDC(wnd, screen);
+ } else {
+ DeleteDC(screen);
+ }
return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer);
@@ -390,7 +424,11 @@ error:
PyErr_SetString(PyExc_OSError, "screen grab failed");
DeleteDC(screen_copy);
- DeleteDC(screen);
+ if (screens == -1) {
+ ReleaseDC(wnd, screen);
+ } else {
+ DeleteDC(screen);
+ }
return NULL;
}
@@ -687,6 +725,14 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) {
#define GET32(p, o) ((DWORD *)(p + o))[0]
+static int CALLBACK
+enhMetaFileProc(
+ HDC hdc, HANDLETABLE *lpht, const ENHMETARECORD *lpmr, int nHandles, LPARAM data
+) {
+ PlayEnhMetaFileRecord(hdc, lpht, lpmr, nHandles);
+ return 1;
+}
+
PyObject *
PyImaging_DrawWmf(PyObject *self, PyObject *args) {
HBITMAP bitmap;
@@ -767,10 +813,7 @@ PyImaging_DrawWmf(PyObject *self, PyObject *args) {
/* FIXME: make background transparent? configurable? */
FillRect(dc, &rect, GetStockObject(WHITE_BRUSH));
- if (!PlayEnhMetaFile(dc, meta, &rect)) {
- PyErr_SetString(PyExc_OSError, "cannot render metafile");
- goto error;
- }
+ EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect);
/* step 4: extract bits from bitmap */
diff --git a/src/encode.c b/src/encode.c
index 13d5cdaf7..7c365a74f 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -27,6 +27,7 @@
#include "thirdparty/pythoncapi_compat.h"
#include "libImaging/Imaging.h"
+#include "libImaging/Bcn.h"
#include "libImaging/Gif.h"
#ifdef HAVE_UNISTD_H
@@ -350,6 +351,31 @@ get_packer(ImagingEncoderObject *encoder, const char *mode, const char *rawmode)
return 0;
}
+/* -------------------------------------------------------------------- */
+/* BCN */
+/* -------------------------------------------------------------------- */
+
+PyObject *
+PyImaging_BcnEncoderNew(PyObject *self, PyObject *args) {
+ ImagingEncoderObject *encoder;
+
+ char *mode;
+ int n;
+ if (!PyArg_ParseTuple(args, "si", &mode, &n)) {
+ return NULL;
+ }
+
+ encoder = PyImaging_EncoderNew(0);
+ if (encoder == NULL) {
+ return NULL;
+ }
+
+ encoder->encode = ImagingBcnEncode;
+ encoder->state.state = n;
+
+ return (PyObject *)encoder;
+}
+
/* -------------------------------------------------------------------- */
/* EPS */
/* -------------------------------------------------------------------- */
diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c
new file mode 100644
index 000000000..33ff2ce77
--- /dev/null
+++ b/src/libImaging/Arrow.c
@@ -0,0 +1,299 @@
+
+#include "Arrow.h"
+#include "Imaging.h"
+#include
+
+/* struct ArrowSchema* */
+/* _arrow_schema_channel(char* channel, char* format) { */
+
+/* } */
+
+static void
+ReleaseExportedSchema(struct ArrowSchema *array) {
+ // This should not be called on already released array
+ // assert(array->release != NULL);
+
+ if (!array->release) {
+ return;
+ }
+ if (array->format) {
+ free((void *)array->format);
+ array->format = NULL;
+ }
+ if (array->name) {
+ free((void *)array->name);
+ array->name = NULL;
+ }
+ if (array->metadata) {
+ free((void *)array->metadata);
+ array->metadata = NULL;
+ }
+
+ // Release children
+ for (int64_t i = 0; i < array->n_children; ++i) {
+ struct ArrowSchema *child = array->children[i];
+ if (child->release != NULL) {
+ child->release(child);
+ child->release = NULL;
+ }
+ // UNDONE -- should I be releasing the children?
+ }
+
+ // Release dictionary
+ struct ArrowSchema *dict = array->dictionary;
+ if (dict != NULL && dict->release != NULL) {
+ dict->release(dict);
+ dict->release = NULL;
+ }
+
+ // TODO here: release and/or deallocate all data directly owned by
+ // the ArrowArray struct, such as the private_data.
+
+ // Mark array released
+ array->release = NULL;
+}
+
+int
+export_named_type(struct ArrowSchema *schema, char *format, char *name) {
+ char *formatp;
+ char *namep;
+ size_t format_len = strlen(format) + 1;
+ size_t name_len = strlen(name) + 1;
+
+ formatp = calloc(format_len, 1);
+
+ if (!formatp) {
+ return IMAGING_CODEC_MEMORY;
+ }
+
+ namep = calloc(name_len, 1);
+ if (!namep) {
+ free(formatp);
+ return IMAGING_CODEC_MEMORY;
+ }
+
+ strncpy(formatp, format, format_len);
+ strncpy(namep, name, name_len);
+
+ *schema = (struct ArrowSchema){// Type description
+ .format = formatp,
+ .name = namep,
+ .metadata = NULL,
+ .flags = 0,
+ .n_children = 0,
+ .children = NULL,
+ .dictionary = NULL,
+ // Bookkeeping
+ .release = &ReleaseExportedSchema
+ };
+ return 0;
+}
+
+int
+export_imaging_schema(Imaging im, struct ArrowSchema *schema) {
+ int retval = 0;
+
+ if (strcmp(im->arrow_band_format, "") == 0) {
+ return IMAGING_ARROW_INCOMPATIBLE_MODE;
+ }
+
+ /* for now, single block images */
+ if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
+ return IMAGING_ARROW_MEMORY_LAYOUT;
+ }
+
+ if (im->bands == 1) {
+ return export_named_type(schema, im->arrow_band_format, im->band_names[0]);
+ }
+
+ retval = export_named_type(schema, "+w:4", "");
+ if (retval != 0) {
+ return retval;
+ }
+ // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands.
+ schema->n_children = 1;
+ schema->children = calloc(1, sizeof(struct ArrowSchema *));
+ schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema));
+ retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel");
+ if (retval != 0) {
+ free(schema->children[0]);
+ schema->release(schema);
+ return retval;
+ }
+ return 0;
+}
+
+static void
+release_const_array(struct ArrowArray *array) {
+ Imaging im = (Imaging)array->private_data;
+
+ if (array->n_children == 0) {
+ ImagingDelete(im);
+ }
+
+ // Free the buffers and the buffers array
+ if (array->buffers) {
+ free(array->buffers);
+ array->buffers = NULL;
+ }
+ if (array->children) {
+ // undone -- does arrow release all the children recursively?
+ for (int i = 0; i < array->n_children; i++) {
+ if (array->children[i]->release) {
+ array->children[i]->release(array->children[i]);
+ array->children[i]->release = NULL;
+ free(array->children[i]);
+ }
+ }
+ free(array->children);
+ array->children = NULL;
+ }
+ // Mark released
+ array->release = NULL;
+}
+
+int
+export_single_channel_array(Imaging im, struct ArrowArray *array) {
+ int length = im->xsize * im->ysize;
+
+ /* for now, single block images */
+ if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
+ return IMAGING_ARROW_MEMORY_LAYOUT;
+ }
+
+ if (im->lines_per_block && im->lines_per_block < im->ysize) {
+ length = im->xsize * im->lines_per_block;
+ }
+
+ MUTEX_LOCK(&im->mutex);
+ im->refcount++;
+ MUTEX_UNLOCK(&im->mutex);
+ // Initialize primitive fields
+ *array = (struct ArrowArray){// Data description
+ .length = length,
+ .offset = 0,
+ .null_count = 0,
+ .n_buffers = 2,
+ .n_children = 0,
+ .children = NULL,
+ .dictionary = NULL,
+ // Bookkeeping
+ .release = &release_const_array,
+ .private_data = im
+ };
+
+ // Allocate list of buffers
+ array->buffers = (const void **)malloc(sizeof(void *) * array->n_buffers);
+ // assert(array->buffers != NULL);
+ array->buffers[0] = NULL; // no nulls, null bitmap can be omitted
+
+ if (im->block) {
+ array->buffers[1] = im->block;
+ } else {
+ array->buffers[1] = im->blocks[0].ptr;
+ }
+ return 0;
+}
+
+int
+export_fixed_pixel_array(Imaging im, struct ArrowArray *array) {
+ int length = im->xsize * im->ysize;
+
+ /* for now, single block images */
+ if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
+ return IMAGING_ARROW_MEMORY_LAYOUT;
+ }
+
+ if (im->lines_per_block && im->lines_per_block < im->ysize) {
+ length = im->xsize * im->lines_per_block;
+ }
+
+ MUTEX_LOCK(&im->mutex);
+ im->refcount++;
+ MUTEX_UNLOCK(&im->mutex);
+ // Initialize primitive fields
+ // Fixed length arrays are 1 buffer of validity, and the length in pixels.
+ // Data is in a child array.
+ *array = (struct ArrowArray){// Data description
+ .length = length,
+ .offset = 0,
+ .null_count = 0,
+ .n_buffers = 1,
+ .n_children = 1,
+ .children = NULL,
+ .dictionary = NULL,
+ // Bookkeeping
+ .release = &release_const_array,
+ .private_data = im
+ };
+
+ // Allocate list of buffers
+ array->buffers = (const void **)calloc(1, sizeof(void *) * array->n_buffers);
+ if (!array->buffers) {
+ goto err;
+ }
+ // assert(array->buffers != NULL);
+ array->buffers[0] = NULL; // no nulls, null bitmap can be omitted
+
+ // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands.
+ array->n_children = 1;
+ array->children = calloc(1, sizeof(struct ArrowArray *));
+ if (!array->children) {
+ goto err;
+ }
+ array->children[0] = (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray));
+ if (!array->children[0]) {
+ goto err;
+ }
+
+ MUTEX_LOCK(&im->mutex);
+ im->refcount++;
+ MUTEX_UNLOCK(&im->mutex);
+ *array->children[0] = (struct ArrowArray){// Data description
+ .length = length * 4,
+ .offset = 0,
+ .null_count = 0,
+ .n_buffers = 2,
+ .n_children = 0,
+ .children = NULL,
+ .dictionary = NULL,
+ // Bookkeeping
+ .release = &release_const_array,
+ .private_data = im
+ };
+
+ array->children[0]->buffers =
+ (const void **)calloc(2, sizeof(void *) * array->n_buffers);
+
+ if (im->block) {
+ array->children[0]->buffers[1] = im->block;
+ } else {
+ array->children[0]->buffers[1] = im->blocks[0].ptr;
+ }
+ return 0;
+
+err:
+ if (array->children[0]) {
+ free(array->children[0]);
+ }
+ if (array->children) {
+ free(array->children);
+ }
+ if (array->buffers) {
+ free(array->buffers);
+ }
+ return IMAGING_CODEC_MEMORY;
+}
+
+int
+export_imaging_array(Imaging im, struct ArrowArray *array) {
+ if (strcmp(im->arrow_band_format, "") == 0) {
+ return IMAGING_ARROW_INCOMPATIBLE_MODE;
+ }
+
+ if (im->bands == 1) {
+ return export_single_channel_array(im, array);
+ }
+
+ return export_fixed_pixel_array(im, array);
+}
diff --git a/src/libImaging/Arrow.h b/src/libImaging/Arrow.h
new file mode 100644
index 000000000..0b285fe80
--- /dev/null
+++ b/src/libImaging/Arrow.h
@@ -0,0 +1,48 @@
+#include
+#include
+
+// Apache License 2.0.
+// Source apache arrow project
+// https://arrow.apache.org/docs/format/CDataInterface.html
+
+#ifndef ARROW_C_DATA_INTERFACE
+#define ARROW_C_DATA_INTERFACE
+
+#define ARROW_FLAG_DICTIONARY_ORDERED 1
+#define ARROW_FLAG_NULLABLE 2
+#define ARROW_FLAG_MAP_KEYS_SORTED 4
+
+struct ArrowSchema {
+ // Array type description
+ const char *format;
+ const char *name;
+ const char *metadata;
+ int64_t flags;
+ int64_t n_children;
+ struct ArrowSchema **children;
+ struct ArrowSchema *dictionary;
+
+ // Release callback
+ void (*release)(struct ArrowSchema *);
+ // Opaque producer-specific data
+ void *private_data;
+};
+
+struct ArrowArray {
+ // Array data description
+ int64_t length;
+ int64_t null_count;
+ int64_t offset;
+ int64_t n_buffers;
+ int64_t n_children;
+ const void **buffers;
+ struct ArrowArray **children;
+ struct ArrowArray *dictionary;
+
+ // Release callback
+ void (*release)(struct ArrowArray *);
+ // Opaque producer-specific data
+ void *private_data;
+};
+
+#endif // ARROW_C_DATA_INTERFACE
diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c
index 9a41febc7..7b3d8f908 100644
--- a/src/libImaging/BcnDecode.c
+++ b/src/libImaging/BcnDecode.c
@@ -25,7 +25,6 @@ typedef struct {
typedef struct {
UINT16 c0, c1;
- UINT32 lut;
} bc1_color;
typedef struct {
@@ -40,13 +39,10 @@ typedef struct {
#define LOAD16(p) (p)[0] | ((p)[1] << 8)
-#define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24)
-
static void
bc1_color_load(bc1_color *dst, const UINT8 *src) {
dst->c0 = LOAD16(src);
dst->c1 = LOAD16(src + 2);
- dst->lut = LOAD32(src + 4);
}
static rgba
@@ -70,7 +66,7 @@ static void
decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) {
bc1_color col;
rgba p[4];
- int n, cw;
+ int n, o, cw;
UINT16 r0, g0, b0, r1, g1, b1;
bc1_color_load(&col, src);
@@ -103,9 +99,11 @@ decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) {
p[3].b = 0;
p[3].a = 0;
}
- for (n = 0; n < 16; n++) {
- cw = 3 & (col.lut >> (2 * n));
- dst[n] = p[cw];
+ for (n = 0; n < 4; n++) {
+ for (o = 0; o < 4; o++) {
+ cw = 3 & ((src + 4)[n] >> (2 * o));
+ dst[n * 4 + o] = p[cw];
+ }
}
}
diff --git a/src/libImaging/BcnEncode.c b/src/libImaging/BcnEncode.c
new file mode 100644
index 000000000..2bad73b92
--- /dev/null
+++ b/src/libImaging/BcnEncode.c
@@ -0,0 +1,298 @@
+/*
+ * The Python Imaging Library
+ *
+ * encoder for DXT1-compressed data
+ *
+ * Format documentation:
+ * https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
+ *
+ */
+
+#include "Imaging.h"
+
+typedef struct {
+ UINT8 color[3];
+} rgb;
+
+typedef struct {
+ UINT8 color[4];
+} rgba;
+
+static rgb
+decode_565(UINT16 x) {
+ rgb item;
+ int r, g, b;
+ r = (x & 0xf800) >> 8;
+ r |= r >> 5;
+ item.color[0] = r;
+ g = (x & 0x7e0) >> 3;
+ g |= g >> 6;
+ item.color[1] = g;
+ b = (x & 0x1f) << 3;
+ b |= b >> 5;
+ item.color[2] = b;
+ return item;
+}
+
+static UINT16
+encode_565(rgba item) {
+ UINT8 r, g, b;
+ r = item.color[0] >> (8 - 5);
+ g = item.color[1] >> (8 - 6);
+ b = item.color[2] >> (8 - 5);
+ return (r << (5 + 6)) | (g << 5) | b;
+}
+
+static void
+encode_bc1_color(Imaging im, ImagingCodecState state, UINT8 *dst, int separate_alpha) {
+ int i, j, k;
+ UINT16 color_min = 0, color_max = 0;
+ rgb color_min_rgb, color_max_rgb;
+ rgba block[16], *current_rgba;
+
+ // Determine the min and max colors in this 4x4 block
+ int first = 1;
+ int transparency = 0;
+ for (i = 0; i < 4; i++) {
+ for (j = 0; j < 4; j++) {
+ current_rgba = &block[i + j * 4];
+
+ int x = state->x + i * im->pixelsize;
+ int y = state->y + j;
+ if (x >= state->xsize * im->pixelsize || y >= state->ysize) {
+ // The 4x4 block extends past the edge of the image
+ for (k = 0; k < 3; k++) {
+ current_rgba->color[k] = 0;
+ }
+ continue;
+ }
+
+ for (k = 0; k < 3; k++) {
+ current_rgba->color[k] =
+ (UINT8)im->image[y][x + (im->pixelsize == 1 ? 0 : k)];
+ }
+ if (separate_alpha) {
+ if ((UINT8)im->image[y][x + 3] == 0) {
+ current_rgba->color[3] = 0;
+ transparency = 1;
+ continue;
+ } else {
+ current_rgba->color[3] = 1;
+ }
+ }
+
+ UINT16 color = encode_565(*current_rgba);
+ if (first || color < color_min) {
+ color_min = color;
+ }
+ if (first || color > color_max) {
+ color_max = color;
+ }
+ first = 0;
+ }
+ }
+
+ if (transparency) {
+ *dst++ = color_min;
+ *dst++ = color_min >> 8;
+ }
+ *dst++ = color_max;
+ *dst++ = color_max >> 8;
+ if (!transparency) {
+ *dst++ = color_min;
+ *dst++ = color_min >> 8;
+ }
+
+ color_min_rgb = decode_565(color_min);
+ color_max_rgb = decode_565(color_max);
+ for (i = 0; i < 4; i++) {
+ UINT8 l = 0;
+ for (j = 3; j > -1; j--) {
+ current_rgba = &block[i * 4 + j];
+ if (transparency && !current_rgba->color[3]) {
+ l |= 3 << (j * 2);
+ continue;
+ }
+
+ float distance = 0;
+ int total = 0;
+ for (k = 0; k < 3; k++) {
+ float denom =
+ (float)abs(color_max_rgb.color[k] - color_min_rgb.color[k]);
+ if (denom != 0) {
+ distance +=
+ abs(current_rgba->color[k] - color_min_rgb.color[k]) / denom;
+ total += 1;
+ }
+ }
+ if (total == 0) {
+ continue;
+ }
+ if (transparency) {
+ distance *= 4 / total;
+ if (distance < 1) {
+ // color_max
+ } else if (distance < 3) {
+ l |= 2 << (j * 2); // 1/2 * color_min + 1/2 * color_max
+ } else {
+ l |= 1 << (j * 2); // color_min
+ }
+ } else {
+ distance *= 6 / total;
+ if (distance < 1) {
+ l |= 1 << (j * 2); // color_min
+ } else if (distance < 3) {
+ l |= 3 << (j * 2); // 1/3 * color_min + 2/3 * color_max
+ } else if (distance < 5) {
+ l |= 2 << (j * 2); // 2/3 * color_min + 1/3 * color_max
+ } else {
+ // color_max
+ }
+ }
+ }
+ *dst++ = l;
+ }
+}
+
+static void
+encode_bc2_block(Imaging im, ImagingCodecState state, UINT8 *dst) {
+ int i, j;
+ UINT8 block[16], current_alpha;
+ for (i = 0; i < 4; i++) {
+ for (j = 0; j < 4; j++) {
+ int x = state->x + i * im->pixelsize;
+ int y = state->y + j;
+ if (x >= state->xsize * im->pixelsize || y >= state->ysize) {
+ // The 4x4 block extends past the edge of the image
+ block[i + j * 4] = 0;
+ continue;
+ }
+
+ current_alpha = (UINT8)im->image[y][x + 3];
+ block[i + j * 4] = current_alpha;
+ }
+ }
+
+ for (i = 0; i < 4; i++) {
+ UINT16 l = 0;
+ for (j = 3; j > -1; j--) {
+ current_alpha = block[i * 4 + j];
+ l |= current_alpha << (j * 4);
+ }
+ *dst++ = l;
+ *dst++ = l >> 8;
+ }
+}
+
+static void
+encode_bc3_alpha(Imaging im, ImagingCodecState state, UINT8 *dst, int o) {
+ int i, j;
+ UINT8 alpha_min = 0, alpha_max = 0;
+ UINT8 block[16], current_alpha;
+
+ // Determine the min and max colors in this 4x4 block
+ int first = 1;
+ for (i = 0; i < 4; i++) {
+ for (j = 0; j < 4; j++) {
+ int x = state->x + i * im->pixelsize;
+ int y = state->y + j;
+ if (x >= state->xsize * im->pixelsize || y >= state->ysize) {
+ // The 4x4 block extends past the edge of the image
+ block[i + j * 4] = 0;
+ continue;
+ }
+
+ current_alpha = (UINT8)im->image[y][x + o];
+ block[i + j * 4] = current_alpha;
+
+ if (first || current_alpha < alpha_min) {
+ alpha_min = current_alpha;
+ }
+ if (first || current_alpha > alpha_max) {
+ alpha_max = current_alpha;
+ }
+ first = 0;
+ }
+ }
+
+ *dst++ = alpha_min;
+ *dst++ = alpha_max;
+
+ float denom = (float)abs(alpha_max - alpha_min);
+ for (i = 0; i < 2; i++) {
+ UINT32 l = 0;
+ for (j = 7; j > -1; j--) {
+ current_alpha = block[i * 8 + j];
+ if (!current_alpha) {
+ l |= 6 << (j * 3);
+ continue;
+ } else if (current_alpha == 255) {
+ l |= 7 << (j * 3);
+ continue;
+ }
+
+ float distance =
+ denom == 0 ? 0 : abs(current_alpha - alpha_min) / denom * 10;
+ if (distance < 3) {
+ l |= 2 << (j * 3); // 4/5 * alpha_min + 1/5 * alpha_max
+ } else if (distance < 5) {
+ l |= 3 << (j * 3); // 3/5 * alpha_min + 2/5 * alpha_max
+ } else if (distance < 7) {
+ l |= 4 << (j * 3); // 2/5 * alpha_min + 3/5 * alpha_max
+ } else {
+ l |= 5 << (j * 3); // 1/5 * alpha_min + 4/5 * alpha_max
+ }
+ }
+ *dst++ = l;
+ *dst++ = l >> 8;
+ *dst++ = l >> 16;
+ }
+}
+
+int
+ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
+ int n = state->state;
+ int has_alpha_channel =
+ strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "LA") == 0;
+
+ UINT8 *dst = buf;
+
+ for (;;) {
+ if (n == 5) {
+ encode_bc3_alpha(im, state, dst, 0);
+ dst += 8;
+
+ encode_bc3_alpha(im, state, dst, 1);
+ } else {
+ if (n == 2 || n == 3) {
+ if (has_alpha_channel) {
+ if (n == 2) {
+ encode_bc2_block(im, state, dst);
+ } else {
+ encode_bc3_alpha(im, state, dst, 3);
+ }
+ dst += 8;
+ } else {
+ for (int i = 0; i < 8; i++) {
+ *dst++ = 0xff;
+ }
+ }
+ }
+ encode_bc1_color(im, state, dst, n == 1 && has_alpha_channel);
+ }
+ dst += 8;
+
+ state->x += im->pixelsize * 4;
+
+ if (state->x >= state->xsize * im->pixelsize) {
+ state->x = 0;
+ state->y += 4;
+ if (state->y >= state->ysize) {
+ state->errcode = IMAGING_CODEC_END;
+ break;
+ }
+ }
+ }
+
+ return dst - buf;
+}
diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c
index ea6f8805e..d5aff8709 100644
--- a/src/libImaging/Draw.c
+++ b/src/libImaging/Draw.c
@@ -501,55 +501,49 @@ polygon_generic(
// Needed to draw consistent polygons
xx[j] = xx[j - 1];
j++;
- } else if (current->dx != 0 && j % 2 == 1 &&
- roundf(xx[j - 1]) == xx[j - 1]) {
+ } else if ((ymin == current->ymin || ymin == current->ymax) &&
+ current->dx != 0) {
// Connect discontiguous corners
for (k = 0; k < i; k++) {
Edge *other_edge = edge_table[k];
- if ((current->dx > 0 && other_edge->dx <= 0) ||
- (current->dx < 0 && other_edge->dx >= 0)) {
+ if ((ymin != other_edge->ymin && ymin != other_edge->ymax) ||
+ other_edge->dx == 0) {
continue;
}
// Check if the two edges join to make a corner
- if (xx[j - 1] ==
- (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) {
+ if (roundf(xx[j - 1]) ==
+ roundf(
+ (ymin - other_edge->y0) * other_edge->dx +
+ other_edge->x0
+ )) {
// Determine points from the edges on the next row
// Or if this is the last row, check the previous row
- int offset = ymin == ymax ? -1 : 1;
+ int offset = ymin == current->ymax ? -1 : 1;
adjacent_line_x =
(ymin + offset - current->y0) * current->dx +
current->x0;
- adjacent_line_x_other_edge =
- (ymin + offset - other_edge->y0) * other_edge->dx +
- other_edge->x0;
- if (ymin == current->ymax) {
- if (current->dx > 0) {
- xx[k] =
- fmax(
+ if (ymin + offset >= other_edge->ymin &&
+ ymin + offset <= other_edge->ymax) {
+ adjacent_line_x_other_edge =
+ (ymin + offset - other_edge->y0) * other_edge->dx +
+ other_edge->x0;
+ if (xx[j - 1] > adjacent_line_x + 1 &&
+ xx[j - 1] > adjacent_line_x_other_edge + 1) {
+ xx[j - 1] =
+ roundf(fmax(
adjacent_line_x, adjacent_line_x_other_edge
- ) +
+ )) +
1;
- } else {
- xx[k] =
- fmin(
+ } else if (xx[j - 1] < adjacent_line_x - 1 &&
+ xx[j - 1] < adjacent_line_x_other_edge - 1) {
+ xx[j - 1] =
+ roundf(fmin(
adjacent_line_x, adjacent_line_x_other_edge
- ) -
- 1;
- }
- } else {
- if (current->dx > 0) {
- xx[k] = fmin(
- adjacent_line_x, adjacent_line_x_other_edge
- );
- } else {
- xx[k] =
- fmax(
- adjacent_line_x, adjacent_line_x_other_edge
- ) +
+ )) -
1;
}
+ break;
}
- break;
}
}
}
diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h
index 0c2d3fc2e..234f9943c 100644
--- a/src/libImaging/Imaging.h
+++ b/src/libImaging/Imaging.h
@@ -20,6 +20,8 @@ extern "C" {
#define M_PI 3.1415926535897932384626433832795
#endif
+#include "Arrow.h"
+
/* -------------------------------------------------------------------- */
/*
@@ -104,6 +106,21 @@ struct ImagingMemoryInstance {
/* Virtual methods */
void (*destroy)(Imaging im);
+
+ /* arrow */
+ int refcount; /* Number of arrow arrays that have been allocated */
+ char band_names[4][3]; /* names of bands, max 2 char + null terminator */
+ char arrow_band_format[2]; /* single character + null terminator */
+
+ int read_only; /* flag for read-only. set for arrow borrowed arrays */
+ PyObject *arrow_array_capsule; /* upstream arrow array source */
+
+ int blocks_count; /* Number of blocks that have been allocated */
+ int lines_per_block; /* Number of lines in a block have been allocated */
+
+#ifdef Py_GIL_DISABLED
+ PyMutex mutex;
+#endif
};
#define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)])
@@ -161,6 +178,7 @@ typedef struct ImagingMemoryArena {
int stats_reallocated_blocks; /* Number of blocks which were actually reallocated
after retrieving */
int stats_freed_blocks; /* Number of freed blocks */
+ int use_block_allocator; /* don't use arena, use block allocator */
#ifdef Py_GIL_DISABLED
PyMutex mutex;
#endif
@@ -174,6 +192,8 @@ extern int
ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max);
extern void
ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size);
+extern void
+ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator);
extern Imaging
ImagingNew(const char *mode, int xsize, int ysize);
@@ -187,6 +207,15 @@ ImagingDelete(Imaging im);
extern Imaging
ImagingNewBlock(const char *mode, int xsize, int ysize);
+extern Imaging
+ImagingNewArrow(
+ const char *mode,
+ int xsize,
+ int ysize,
+ PyObject *schema_capsule,
+ PyObject *array_capsule
+);
+
extern Imaging
ImagingNewPrologue(const char *mode, int xsize, int ysize);
extern Imaging
@@ -567,6 +596,8 @@ typedef int (*ImagingCodec)(
extern int
ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes);
extern int
+ImagingBcnEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes);
+extern int
ImagingBitDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes);
extern int
ImagingEpsEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes);
@@ -698,6 +729,13 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence);
extern Py_ssize_t
_imaging_tell_pyFd(PyObject *fd);
+/* Arrow */
+
+extern int
+export_imaging_array(Imaging im, struct ArrowArray *array);
+extern int
+export_imaging_schema(Imaging im, struct ArrowSchema *schema);
+
/* Errcodes */
#define IMAGING_CODEC_END 1
#define IMAGING_CODEC_OVERRUN -1
@@ -705,6 +743,8 @@ _imaging_tell_pyFd(PyObject *fd);
#define IMAGING_CODEC_UNKNOWN -3
#define IMAGING_CODEC_CONFIG -8
#define IMAGING_CODEC_MEMORY -9
+#define IMAGING_ARROW_INCOMPATIBLE_MODE -10
+#define IMAGING_ARROW_MEMORY_LAYOUT -11
#include "ImagingUtils.h"
extern UINT8 *clip8_lookups;
diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c
index 4f185b529..cc6955ca5 100644
--- a/src/libImaging/Jpeg2KDecode.c
+++ b/src/libImaging/Jpeg2KDecode.c
@@ -615,6 +615,7 @@ static const struct j2k_decode_unpacker j2k_unpackers[] = {
{"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la},
{"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb},
{"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb},
+ {"RGBA", OPJ_CLRSPC_GRAY, 4, 1, j2ku_srgba_rgba},
{"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba},
{"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba},
{"CMYK", OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba},
diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h
index 0462cfd49..d75d55ce0 100644
--- a/src/libImaging/QuantHash.h
+++ b/src/libImaging/QuantHash.h
@@ -20,8 +20,12 @@ typedef uint32_t HashVal_t;
typedef uint32_t (*HashFunc)(const HashTable *, const HashKey_t);
typedef int (*HashCmpFunc)(const HashTable *, const HashKey_t, const HashKey_t);
-typedef void (*IteratorFunc)(const HashTable *, const HashKey_t, const HashVal_t, void *);
-typedef void (*IteratorUpdateFunc)(const HashTable *, const HashKey_t, HashVal_t *, void *);
+typedef void (*IteratorFunc)(
+ const HashTable *, const HashKey_t, const HashVal_t, void *
+);
+typedef void (*IteratorUpdateFunc)(
+ const HashTable *, const HashKey_t, HashVal_t *, void *
+);
typedef void (*ComputeFunc)(const HashTable *, const HashKey_t, HashVal_t *);
typedef void (*CollisionFunc)(
const HashTable *, HashKey_t *, HashVal_t *, HashKey_t, HashVal_t
diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c
index 522e9f375..4fa4ecd1c 100644
--- a/src/libImaging/Storage.c
+++ b/src/libImaging/Storage.c
@@ -58,19 +58,22 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
/* Setup image descriptor */
im->xsize = xsize;
im->ysize = ysize;
-
+ im->refcount = 1;
im->type = IMAGING_TYPE_UINT8;
+ strcpy(im->arrow_band_format, "C");
if (strcmp(mode, "1") == 0) {
/* 1-bit images */
im->bands = im->pixelsize = 1;
im->linesize = xsize;
+ strcpy(im->band_names[0], "1");
} else if (strcmp(mode, "P") == 0) {
/* 8-bit palette mapped images */
im->bands = im->pixelsize = 1;
im->linesize = xsize;
im->palette = ImagingPaletteNew("RGB");
+ strcpy(im->band_names[0], "P");
} else if (strcmp(mode, "PA") == 0) {
/* 8-bit palette with alpha */
@@ -78,23 +81,36 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 4; /* store in image32 memory */
im->linesize = xsize * 4;
im->palette = ImagingPaletteNew("RGB");
+ strcpy(im->band_names[0], "P");
+ strcpy(im->band_names[1], "X");
+ strcpy(im->band_names[2], "X");
+ strcpy(im->band_names[3], "A");
} else if (strcmp(mode, "L") == 0) {
/* 8-bit grayscale (luminance) images */
im->bands = im->pixelsize = 1;
im->linesize = xsize;
+ strcpy(im->band_names[0], "L");
} else if (strcmp(mode, "LA") == 0) {
/* 8-bit grayscale (luminance) with alpha */
im->bands = 2;
im->pixelsize = 4; /* store in image32 memory */
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "L");
+ strcpy(im->band_names[1], "X");
+ strcpy(im->band_names[2], "X");
+ strcpy(im->band_names[3], "A");
} else if (strcmp(mode, "La") == 0) {
/* 8-bit grayscale (luminance) with premultiplied alpha */
im->bands = 2;
im->pixelsize = 4; /* store in image32 memory */
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "L");
+ strcpy(im->band_names[1], "X");
+ strcpy(im->band_names[2], "X");
+ strcpy(im->band_names[3], "a");
} else if (strcmp(mode, "F") == 0) {
/* 32-bit floating point images */
@@ -102,6 +118,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 4;
im->linesize = xsize * 4;
im->type = IMAGING_TYPE_FLOAT32;
+ strcpy(im->arrow_band_format, "f");
+ strcpy(im->band_names[0], "F");
} else if (strcmp(mode, "I") == 0) {
/* 32-bit integer images */
@@ -109,6 +127,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 4;
im->linesize = xsize * 4;
im->type = IMAGING_TYPE_INT32;
+ strcpy(im->arrow_band_format, "i");
+ strcpy(im->band_names[0], "I");
} else if (strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 ||
strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) {
@@ -118,12 +138,18 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 2;
im->linesize = xsize * 2;
im->type = IMAGING_TYPE_SPECIAL;
+ strcpy(im->arrow_band_format, "s");
+ strcpy(im->band_names[0], "I");
} else if (strcmp(mode, "RGB") == 0) {
/* 24-bit true colour images */
im->bands = 3;
im->pixelsize = 4;
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "R");
+ strcpy(im->band_names[1], "G");
+ strcpy(im->band_names[2], "B");
+ strcpy(im->band_names[3], "X");
} else if (strcmp(mode, "BGR;15") == 0) {
/* EXPERIMENTAL */
@@ -132,6 +158,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 2;
im->linesize = (xsize * 2 + 3) & -4;
im->type = IMAGING_TYPE_SPECIAL;
+ /* not allowing arrow due to line length packing */
+ strcpy(im->arrow_band_format, "");
} else if (strcmp(mode, "BGR;16") == 0) {
/* EXPERIMENTAL */
@@ -140,6 +168,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 2;
im->linesize = (xsize * 2 + 3) & -4;
im->type = IMAGING_TYPE_SPECIAL;
+ /* not allowing arrow due to line length packing */
+ strcpy(im->arrow_band_format, "");
} else if (strcmp(mode, "BGR;24") == 0) {
/* EXPERIMENTAL */
@@ -148,32 +178,54 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 3;
im->linesize = (xsize * 3 + 3) & -4;
im->type = IMAGING_TYPE_SPECIAL;
+ /* not allowing arrow due to line length packing */
+ strcpy(im->arrow_band_format, "");
} else if (strcmp(mode, "RGBX") == 0) {
/* 32-bit true colour images with padding */
im->bands = im->pixelsize = 4;
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "R");
+ strcpy(im->band_names[1], "G");
+ strcpy(im->band_names[2], "B");
+ strcpy(im->band_names[3], "X");
} else if (strcmp(mode, "RGBA") == 0) {
/* 32-bit true colour images with alpha */
im->bands = im->pixelsize = 4;
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "R");
+ strcpy(im->band_names[1], "G");
+ strcpy(im->band_names[2], "B");
+ strcpy(im->band_names[3], "A");
} else if (strcmp(mode, "RGBa") == 0) {
/* 32-bit true colour images with premultiplied alpha */
im->bands = im->pixelsize = 4;
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "R");
+ strcpy(im->band_names[1], "G");
+ strcpy(im->band_names[2], "B");
+ strcpy(im->band_names[3], "a");
} else if (strcmp(mode, "CMYK") == 0) {
/* 32-bit colour separation */
im->bands = im->pixelsize = 4;
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "C");
+ strcpy(im->band_names[1], "M");
+ strcpy(im->band_names[2], "Y");
+ strcpy(im->band_names[3], "K");
} else if (strcmp(mode, "YCbCr") == 0) {
/* 24-bit video format */
im->bands = 3;
im->pixelsize = 4;
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "Y");
+ strcpy(im->band_names[1], "Cb");
+ strcpy(im->band_names[2], "Cr");
+ strcpy(im->band_names[3], "X");
} else if (strcmp(mode, "LAB") == 0) {
/* 24-bit color, luminance, + 2 color channels */
@@ -181,6 +233,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->bands = 3;
im->pixelsize = 4;
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "L");
+ strcpy(im->band_names[1], "a");
+ strcpy(im->band_names[2], "b");
+ strcpy(im->band_names[3], "X");
} else if (strcmp(mode, "HSV") == 0) {
/* 24-bit color, luminance, + 2 color channels */
@@ -188,6 +244,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->bands = 3;
im->pixelsize = 4;
im->linesize = xsize * 4;
+ strcpy(im->band_names[0], "H");
+ strcpy(im->band_names[1], "S");
+ strcpy(im->band_names[2], "V");
+ strcpy(im->band_names[3], "X");
} else {
free(im);
@@ -218,6 +278,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
break;
}
+ // UNDONE -- not accurate for arrow
MUTEX_LOCK(&ImagingDefaultArena.mutex);
ImagingDefaultArena.stats_new_count += 1;
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
@@ -238,8 +299,18 @@ ImagingDelete(Imaging im) {
return;
}
+ MUTEX_LOCK(&im->mutex);
+ im->refcount--;
+
+ if (im->refcount > 0) {
+ MUTEX_UNLOCK(&im->mutex);
+ return;
+ }
+ MUTEX_UNLOCK(&im->mutex);
+
if (im->palette) {
ImagingPaletteDelete(im->palette);
+ im->palette = NULL;
}
if (im->destroy) {
@@ -270,6 +341,7 @@ struct ImagingMemoryArena ImagingDefaultArena = {
0,
0,
0, // Stats
+ 0, // use_block_allocator
#ifdef Py_GIL_DISABLED
{0},
#endif
@@ -302,6 +374,11 @@ ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) {
return 1;
}
+void
+ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator) {
+ arena->use_block_allocator = use_block_allocator;
+}
+
void
ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) {
while (arena->blocks_cached > new_size) {
@@ -396,11 +473,13 @@ ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_
if (lines_per_block == 0) {
lines_per_block = 1;
}
+ im->lines_per_block = lines_per_block;
blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block;
// printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n",
// im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count);
/* One extra pointer is always NULL */
+ im->blocks_count = blocks_count;
im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1);
if (!im->blocks) {
return (Imaging)ImagingError_MemoryError();
@@ -487,6 +566,58 @@ ImagingAllocateBlock(Imaging im) {
return im;
}
+/* Borrowed Arrow Storage Type */
+/* --------------------------- */
+/* Don't allocate the image. */
+
+static void
+ImagingDestroyArrow(Imaging im) {
+ // Rely on the internal Python destructor for the array capsule.
+ if (im->arrow_array_capsule) {
+ Py_DECREF(im->arrow_array_capsule);
+ im->arrow_array_capsule = NULL;
+ }
+}
+
+Imaging
+ImagingBorrowArrow(
+ Imaging im,
+ struct ArrowArray *external_array,
+ int offset_width,
+ PyObject *arrow_capsule
+) {
+ // offset_width is the number of char* for a single offset from arrow
+ Py_ssize_t y, i;
+
+ char *borrowed_buffer = NULL;
+ struct ArrowArray *arr = external_array;
+
+ if (arr->n_children == 1) {
+ arr = arr->children[0];
+ }
+ if (arr->n_buffers == 2) {
+ // buffer 0 is the null list
+ // buffer 1 is the data
+ borrowed_buffer = (char *)arr->buffers[1] + (offset_width * arr->offset);
+ }
+
+ if (!borrowed_buffer) {
+ return (Imaging
+ )ImagingError_ValueError("Arrow Array, exactly 2 buffers required");
+ }
+
+ for (y = i = 0; y < im->ysize; y++) {
+ im->image[y] = borrowed_buffer + i;
+ i += im->linesize;
+ }
+ im->read_only = 1;
+ Py_INCREF(arrow_capsule);
+ im->arrow_array_capsule = arrow_capsule;
+ im->destroy = ImagingDestroyArrow;
+
+ return im;
+}
+
/* --------------------------------------------------------------------
* Create a new, internally allocated, image.
*/
@@ -529,11 +660,17 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) {
Imaging
ImagingNew(const char *mode, int xsize, int ysize) {
+ if (ImagingDefaultArena.use_block_allocator) {
+ return ImagingNewBlock(mode, xsize, ysize);
+ }
return ImagingNewInternal(mode, xsize, ysize, 0);
}
Imaging
ImagingNewDirty(const char *mode, int xsize, int ysize) {
+ if (ImagingDefaultArena.use_block_allocator) {
+ return ImagingNewBlock(mode, xsize, ysize);
+ }
return ImagingNewInternal(mode, xsize, ysize, 1);
}
@@ -558,6 +695,66 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) {
return NULL;
}
+Imaging
+ImagingNewArrow(
+ const char *mode,
+ int xsize,
+ int ysize,
+ PyObject *schema_capsule,
+ PyObject *array_capsule
+) {
+ /* A borrowed arrow array */
+ Imaging im;
+ struct ArrowSchema *schema =
+ (struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema");
+
+ struct ArrowArray *external_array =
+ (struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array");
+
+ if (xsize < 0 || ysize < 0) {
+ return (Imaging)ImagingError_ValueError("bad image size");
+ }
+
+ im = ImagingNewPrologue(mode, xsize, ysize);
+ if (!im) {
+ return NULL;
+ }
+
+ int64_t pixels = (int64_t)xsize * (int64_t)ysize;
+
+ // fmt:off // don't reformat this
+ if (((strcmp(schema->format, "I") == 0 // int32
+ && im->pixelsize == 4 // 4xchar* storage
+ && im->bands >= 2) // INT32 into any INT32 Storage mode
+ || // (()||()) &&
+ (strcmp(schema->format, im->arrow_band_format) == 0 // same mode
+ && im->bands == 1)) // Single band match
+ && pixels == external_array->length) {
+ // one arrow element per, and it matches a pixelsize*char
+ if (ImagingBorrowArrow(im, external_array, im->pixelsize, array_capsule)) {
+ return im;
+ }
+ }
+ if (strcmp(schema->format, "+w:4") == 0 // 4 up array
+ && im->pixelsize == 4 // storage as 32 bpc
+ && schema->n_children > 0 // make sure schema is well formed.
+ && schema->children // make sure schema is well formed
+ && strcmp(schema->children[0]->format, "C") == 0 // Expected format
+ && strcmp(im->arrow_band_format, "C") == 0 // Expected Format
+ && pixels == external_array->length // expected length
+ && external_array->n_children == 1 // array is well formed
+ && external_array->children // array is well formed
+ && 4 * pixels == external_array->children[0]->length) {
+ // 4 up element of char into pixelsize == 4
+ if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) {
+ return im;
+ }
+ }
+ // fmt: on
+ ImagingDelete(im);
+ return NULL;
+}
+
Imaging
ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) {
/* allocate or validate output image */
diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c
index e4da9162d..9a2db95b4 100644
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -299,6 +299,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) {
return -1;
}
+ img.orientation = ORIENTATION_TOPLEFT;
img.req_orientation = ORIENTATION_TOPLEFT;
img.col_offset = 0;
diff --git a/src/path.c b/src/path.c
index 5affe3a1f..38300547c 100644
--- a/src/path.c
+++ b/src/path.c
@@ -109,6 +109,39 @@ path_dealloc(PyPathObject *path) {
#define PyPath_Check(op) (Py_TYPE(op) == &PyPathType)
+static int
+assign_item_to_array(double *xy, Py_ssize_t j, PyObject *op) {
+ if (PyFloat_Check(op)) {
+ xy[j++] = PyFloat_AS_DOUBLE(op);
+ } else if (PyLong_Check(op)) {
+ xy[j++] = (float)PyLong_AS_LONG(op);
+ } else if (PyNumber_Check(op)) {
+ xy[j++] = PyFloat_AsDouble(op);
+ } else if (PyList_Check(op)) {
+ for (int k = 0; k < 2; k++) {
+ PyObject *op1 = PyList_GetItemRef(op, k);
+ if (op1 == NULL) {
+ return -1;
+ }
+ j = assign_item_to_array(xy, j, op1);
+ Py_DECREF(op1);
+ if (j == -1) {
+ return -1;
+ }
+ }
+ } else {
+ double x, y;
+ if (PyArg_ParseTuple(op, "dd", &x, &y)) {
+ xy[j++] = x;
+ xy[j++] = y;
+ } else {
+ PyErr_SetString(PyExc_ValueError, "incorrect coordinate type");
+ return -1;
+ }
+ }
+ return j;
+}
+
Py_ssize_t
PyPath_Flatten(PyObject *data, double **pxy) {
Py_ssize_t i, j, n;
@@ -164,48 +197,32 @@ PyPath_Flatten(PyObject *data, double **pxy) {
return -1;
}
-#define assign_item_to_array(op, decref) \
- if (PyFloat_Check(op)) { \
- xy[j++] = PyFloat_AS_DOUBLE(op); \
- } else if (PyLong_Check(op)) { \
- xy[j++] = (float)PyLong_AS_LONG(op); \
- } else if (PyNumber_Check(op)) { \
- xy[j++] = PyFloat_AsDouble(op); \
- } else if (PyArg_ParseTuple(op, "dd", &x, &y)) { \
- xy[j++] = x; \
- xy[j++] = y; \
- } else { \
- PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); \
- if (decref) { \
- Py_DECREF(op); \
- } \
- free(xy); \
- return -1; \
- } \
- if (decref) { \
- Py_DECREF(op); \
- }
-
/* Copy table to path array */
if (PyList_Check(data)) {
for (i = 0; i < n; i++) {
- double x, y;
PyObject *op = PyList_GetItemRef(data, i);
if (op == NULL) {
free(xy);
return -1;
}
- assign_item_to_array(op, 1);
+ j = assign_item_to_array(xy, j, op);
+ Py_DECREF(op);
+ if (j == -1) {
+ free(xy);
+ return -1;
+ }
}
} else if (PyTuple_Check(data)) {
for (i = 0; i < n; i++) {
- double x, y;
PyObject *op = PyTuple_GET_ITEM(data, i);
- assign_item_to_array(op, 0);
+ j = assign_item_to_array(xy, j, op);
+ if (j == -1) {
+ free(xy);
+ return -1;
+ }
}
} else {
for (i = 0; i < n; i++) {
- double x, y;
PyObject *op = PySequence_GetItem(data, i);
if (!op) {
/* treat IndexError as end of sequence */
@@ -217,7 +234,12 @@ PyPath_Flatten(PyObject *data, double **pxy) {
return -1;
}
}
- assign_item_to_array(op, 1);
+ j = assign_item_to_array(xy, j, op);
+ Py_DECREF(op);
+ if (j == -1) {
+ free(xy);
+ return -1;
+ }
}
}
diff --git a/wheels/dependency_licenses/FREETYPE2.txt b/wheels/dependency_licenses/FREETYPE2.txt
index 93efc6126..8f2345992 100644
--- a/wheels/dependency_licenses/FREETYPE2.txt
+++ b/wheels/dependency_licenses/FREETYPE2.txt
@@ -211,351 +211,6 @@ Legal Terms
--- end of FTL.TXT ---
---------------------------------------------------------------------------
-
- GNU GENERAL PUBLIC LICENSE
- Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.
- 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The licenses for most software are designed to take away your
-freedom to share and change it. By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users. This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it. (Some other Free Software Foundation software is covered by
-the GNU Library General Public License instead.) You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
- To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have. You must make sure that they, too, receive or can get the
-source code. And you must show them these terms so they know their
-rights.
-
- We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
- Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software. If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
- Finally, any free program is threatened constantly by software
-patents. We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary. To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- GNU GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License. The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language. (Hereinafter, translation is included without limitation in
-the term "modification".) Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope. The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
- 1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
- 2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
- a) You must cause the modified files to carry prominent notices
- stating that you changed the files and the date of any change.
-
- b) You must cause any work that you distribute or publish, that in
- whole or in part contains or is derived from the Program or any
- part thereof, to be licensed as a whole at no charge to all third
- parties under the terms of this License.
-
- c) If the modified program normally reads commands interactively
- when run, you must cause it, when started running for such
- interactive use in the most ordinary way, to print or display an
- announcement including an appropriate copyright notice and a
- notice that there is no warranty (or else, saying that you provide
- a warranty) and that users may redistribute the program under
- these conditions, and telling the user how to view a copy of this
- License. (Exception: if the Program itself is interactive but
- does not normally print such an announcement, your work based on
- the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole. If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works. But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
- 3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
- a) Accompany it with the complete corresponding machine-readable
- source code, which must be distributed under the terms of Sections
- 1 and 2 above on a medium customarily used for software interchange; or,
-
- b) Accompany it with a written offer, valid for at least three
- years, to give any third party, for a charge no more than your
- cost of physically performing source distribution, a complete
- machine-readable copy of the corresponding source code, to be
- distributed under the terms of Sections 1 and 2 above on a medium
- customarily used for software interchange; or,
-
- c) Accompany it with the information you received as to the offer
- to distribute corresponding source code. (This alternative is
- allowed only for noncommercial distribution and only if you
- received the program in object code or executable form with such
- an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it. For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable. However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
- 4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License. Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
- 5. You are not required to accept this License, since you have not
-signed it. However, nothing else grants you permission to modify or
-distribute the Program or its derivative works. These actions are
-prohibited by law if you do not accept this License. Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
- 6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions. You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
- 7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all. For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices. Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
- 8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded. In such case, this License incorporates
-the limitation as if written in the body of this License.
-
- 9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation. If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
- 10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission. For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this. Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
- NO WARRANTY
-
- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
- 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
- Gnomovision version 69, Copyright (C) year name of author
- Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary. Here is a sample; alter the names:
-
- Yoyodyne, Inc., hereby disclaims all copyright interest in the program
- `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
- , 1 April 1989
- Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs. If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library. If this is what you want to do, use the GNU Library General
-Public License instead of this License.
-
---------------------------------------------------------------------------
-
The following license details are part of `src/bdf/README`:
```
diff --git a/winbuild/build.rst b/winbuild/build.rst
index aae78ce12..3c20c7d17 100644
--- a/winbuild/build.rst
+++ b/winbuild/build.rst
@@ -61,6 +61,7 @@ Run ``build_prepare.py`` to configure the build::
--no-imagequant skip GPL-licensed optional dependency libimagequant
--no-fribidi, --no-raqm
skip LGPL-licensed optional dependency FriBiDi
+ --no-avif skip optional dependency libavif
Arguments can also be supplied using the environment variables PILLOW_BUILD,
PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information.
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index a645722d8..17fc37572 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -113,15 +113,16 @@ V = {
"BROTLI": "1.1.0",
"FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16",
- "HARFBUZZ": "10.4.0",
+ "HARFBUZZ": "11.1.0",
"JPEGTURBO": "3.1.0",
"LCMS2": "2.17",
+ "LIBAVIF": "1.2.1",
"LIBIMAGEQUANT": "4.3.4",
"LIBPNG": "1.6.47",
"LIBWEBP": "1.5.0",
"OPENJPEG": "2.5.3",
- "TIFF": "4.6.0",
- "XZ": "5.6.4",
+ "TIFF": "4.7.0",
+ "XZ": "5.8.1",
"ZLIBNG": "2.2.4",
}
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
@@ -180,7 +181,11 @@ DEPS: dict[str, dict[str, Any]] = {
"filename": f"xz-{V['XZ']}.tar.gz",
"license": "COPYING",
"build": [
- *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),
+ *cmds_cmake(
+ "liblzma",
+ "-DBUILD_SHARED_LIBS:BOOL=OFF"
+ + (" -DXZ_CLMUL_CRC:BOOL=OFF" if struct.calcsize("P") == 4 else ""),
+ ),
cmd_mkdir(r"{inc_dir}\lzma"),
cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"),
],
@@ -234,6 +239,7 @@ DEPS: dict[str, dict[str, Any]] = {
"-DBUILD_SHARED_LIBS:BOOL=OFF",
"-DWebP_LIBRARY=libwebp",
'-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"',
+ "-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
)
],
"headers": [r"libtiff\tiff*.h"],
@@ -347,8 +353,8 @@ DEPS: dict[str, dict[str, Any]] = {
"libs": [r"..\target\release\imagequant_sys.lib"],
},
"harfbuzz": {
- "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip",
- "filename": f"harfbuzz-{V['HARFBUZZ']}.zip",
+ "url": f"https://github.com/harfbuzz/harfbuzz/releases/download/{V['HARFBUZZ']}/FILENAME",
+ "filename": f"harfbuzz-{V['HARFBUZZ']}.tar.xz",
"license": "COPYING",
"build": [
*cmds_cmake(
@@ -378,6 +384,27 @@ DEPS: dict[str, dict[str, Any]] = {
],
"bins": [r"*.dll"],
},
+ "libavif": {
+ "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip",
+ "filename": f"libavif-{V['LIBAVIF']}.zip",
+ "license": "LICENSE",
+ "build": [
+ f"{sys.executable} -m pip install meson",
+ *cmds_cmake(
+ "avif_static",
+ "-DBUILD_SHARED_LIBS=OFF",
+ "-DAVIF_LIBSHARPYUV=LOCAL",
+ "-DAVIF_LIBYUV=LOCAL",
+ "-DAVIF_CODEC_AOM=LOCAL",
+ "-DAVIF_CODEC_DAV1D=LOCAL",
+ "-DAVIF_CODEC_RAV1E=LOCAL",
+ "-DAVIF_CODEC_SVT=LOCAL",
+ "-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
+ ),
+ cmd_xcopy("include", "{inc_dir}"),
+ ],
+ "libs": ["avif.lib"],
+ },
}
@@ -491,8 +518,8 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None:
msg = "Attempted Path Traversal in Zip File"
raise RuntimeError(msg)
zf.extractall(sources_dir)
- elif filename.endswith((".tar.gz", ".tgz")):
- with tarfile.open(file, "r:gz") as tgz:
+ elif filename.endswith((".tar.gz", ".tar.xz")):
+ with tarfile.open(file, "r:xz" if filename.endswith(".xz") else "r:gz") as tgz:
for member in tgz.getnames():
member_abspath = os.path.abspath(os.path.join(sources_dir, member))
member_prefix = os.path.commonpath([sources_dir_abs, member_abspath])
@@ -683,6 +710,11 @@ def main() -> None:
action="store_true",
help="skip LGPL-licensed optional dependency FriBiDi",
)
+ parser.add_argument(
+ "--no-avif",
+ action="store_true",
+ help="skip optional dependency libavif",
+ )
args = parser.parse_args()
arch_prefs = ARCHITECTURES[args.architecture]
@@ -723,6 +755,8 @@ def main() -> None:
disabled += ["libimagequant"]
if args.no_fribidi:
disabled += ["fribidi"]
+ if args.no_avif or args.architecture != "AMD64":
+ disabled += ["libavif"]
prefs = {
"architecture": args.architecture,
@@ -746,7 +780,7 @@ def main() -> None:
for k, v in DEPS.items():
if "dir" not in v:
- v["dir"] = re.sub(r"\.(tar\.gz|zip)", "", v["filename"])
+ v["dir"] = re.sub(r"\.(tar\.gz|tar\.xz|zip)", "", v["filename"])
prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"])
print()