diff --git a/.appveyor.yml b/.appveyor.yml
index cc4d56d0b..4c5a7f9ee 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -10,11 +10,11 @@ environment:
TEST_OPTIONS:
DEPLOY: YES
matrix:
- - PYTHON: C:/Python311
+ - PYTHON: C:/Python312
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64
- ARCHITECTURE: x64
+ ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
@@ -43,7 +43,7 @@ build_script:
test_script:
- cd c:\pillow
-- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
+- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
diff --git a/.ci/install.sh b/.ci/install.sh
index 4748feb3d..30b64349d 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -28,7 +28,8 @@ fi
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
-PYTHONOPTIMIZE=0 python3 -m pip install cffi
+# TODO Update condition when cffi supports 3.13
+if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
@@ -38,7 +39,8 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
- python3 -m pip install numpy
+ # TODO Update condition when NumPy supports 3.13
+ if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
@@ -46,6 +48,16 @@ if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install pyqt6
fi
+ # Pyroma uses non-isolated build and fails with old setuptools
+ if [[
+ $GHA_PYTHON_VERSION == pypy3.9
+ || $GHA_PYTHON_VERSION == 3.8
+ || $GHA_PYTHON_VERSION == 3.9
+ ]]; then
+ # To match pyproject.toml
+ python3 -m pip install "setuptools>=67.8"
+ fi
+
# webp
pushd depends && ./install_webp.sh && popd
diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt
new file mode 100644
index 000000000..ccd6d87ed
--- /dev/null
+++ b/.ci/requirements-cibw.txt
@@ -0,0 +1 @@
+cibuildwheel==2.16.5
diff --git a/.coveragerc b/.coveragerc
index f71b6b1a2..46df3f90d 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,15 +2,14 @@
[report]
# Regexes for lines to exclude from consideration
-exclude_lines =
- # Have to re-enable the standard pragma:
- pragma: no cover
-
- # Don't complain if non-runnable code isn't run:
+exclude_also =
+ # Don't complain if non-runnable code isn't run
if 0:
if __name__ == .__main__.:
# Don't complain about debug code
if DEBUG:
+ # Don't complain about compatibility code for missing optional dependencies
+ except ImportError
[run]
omit =
diff --git a/.editorconfig b/.editorconfig
index d74549fe2..c3627ae4f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,7 +13,7 @@ indent_style = space
trim_trailing_whitespace = true
-[*.yml]
+[*.{toml,yml}]
# Two-space indentation
indent_size = 2
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 000000000..a2be59c52
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,6 @@
+# Flake8
+8de95676e0fd89f2326b3953488ab66ff29cd2d0
+# Format with Black
+53a7e3500437a9fd5826bc04758f7116bd7e52dc
+# Format the C code with ClangFormat
+46b7e86bab79450ec0a2866c6c0c679afb659d17
diff --git a/.github/problem-matchers/gcc.json b/.github/problem-matchers/gcc.json
new file mode 100644
index 000000000..8e2866afe
--- /dev/null
+++ b/.github/problem-matchers/gcc.json
@@ -0,0 +1,18 @@
+{
+ "__comment": "Based on vscode-cpptools' Extension/package.json gcc rule",
+ "problemMatcher": [
+ {
+ "owner": "gcc-problem-matcher",
+ "pattern": [
+ {
+ "regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
+ "file": 1,
+ "line": 2,
+ "column": 3,
+ "severity": 4,
+ "message": 5
+ }
+ ]
+ }
+ ]
+}
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
index 4d855469a..3711d91f0 100644
--- a/.github/release-drafter.yml
+++ b/.github/release-drafter.yml
@@ -13,6 +13,8 @@ categories:
label: "Removal"
- title: "Testing"
label: "Testing"
+ - title: "Type hints"
+ label: "Type hints"
exclude-labels:
- "changelog: skip"
diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml
index 55a672dd8..eb73fc6a7 100644
--- a/.github/workflows/cifuzz.yml
+++ b/.github/workflows/cifuzz.yml
@@ -42,13 +42,13 @@ jobs:
language: python
dry-run: false
- name: Upload New Crash
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts
- name: Upload Legacy Crash
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: steps.run.outcome == 'success'
with:
name: crash
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 8968de72e..4319cc8ff 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -33,20 +33,30 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: pip
- cache-dependency-path: ".ci/*.sh"
+ cache-dependency-path: |
+ ".ci/*.sh"
+ "pyproject.toml"
- name: Build system information
run: python3 .github/workflows/system-info.py
+ - name: Cache libimagequant
+ uses: actions/cache@v4
+ id: cache-libimagequant
+ with:
+ path: ~/cache-libimagequant
+ key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
+
- name: Install Linux dependencies
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: "3.x"
+ GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
- name: Build
run: |
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 78b80d26e..cc4760288 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,6 +2,9 @@ name: Lint
on: [push, pull_request, workflow_dispatch]
+env:
+ FORCE_COLOR: 1
+
permissions:
contents: read
@@ -20,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
- name: pre-commit cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
@@ -28,7 +31,7 @@ jobs:
lint-pre-commit-
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: pip
@@ -46,3 +49,6 @@ jobs:
run: tox -e lint
env:
PRE_COMMIT_COLOR: always
+
+ - name: Mypy
+ run: tox -e mypy
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index a20838a15..28124d7f7 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -2,10 +2,21 @@
set -e
-brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
+brew install \
+ freetype \
+ ghostscript \
+ libimagequant \
+ libjpeg \
+ libraqm \
+ libtiff \
+ little-cms2 \
+ openjpeg \
+ webp
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
-PYTHONOPTIMIZE=0 python3 -m pip install cffi
+# TODO Update condition when cffi supports 3.13
+if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
+
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile
@@ -14,7 +25,8 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-python3 -m pip install numpy
+# TODO Update condition when NumPy supports 3.13
+if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 31f63e1c6..545c2e364 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- name: "Check issues"
- uses: actions/stale@v8
+ uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action"
diff --git a/.github/workflows/system-info.py b/.github/workflows/system-info.py
index 8e840319a..57f28c620 100644
--- a/.github/workflows/system-info.py
+++ b/.github/workflows/system-info.py
@@ -6,6 +6,8 @@ This sort of info is missing from GitHub Actions.
Requested here:
https://github.com/actions/virtual-environments/issues/79
"""
+from __future__ import annotations
+
import os
import platform
import sys
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 10de3b9fb..b5c8c39aa 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
@@ -49,7 +47,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Cygwin
- uses: cygwin/cygwin-install-action@v4
+ uses: egor-tensin/setup-cygwin@v4
with:
platform: x86_64
packages: >
@@ -71,6 +69,7 @@ jobs:
make
netpbm
perl
+ python39=3.9.16-1
python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
@@ -88,7 +87,7 @@ jobs:
- name: Select Python version
run: |
- ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
+ ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/cygwin/bin/python3
- name: Get latest NumPy version
id: latest-numpy
@@ -97,7 +96,7 @@ jobs:
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
- name: pip cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: 'C:\cygwin\home\runneradmin\.cache\pip'
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
@@ -132,7 +131,7 @@ jobs:
dash.exe -c "mkdir -p Tests/errors"
- name: Upload errors
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: failure()
with:
name: errors
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index c8fd69ba0..3bb6856f6 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
@@ -51,8 +49,8 @@ jobs:
debian-11-bullseye-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
- fedora-37-amd64,
fedora-38-amd64,
+ fedora-39-amd64,
gentoo,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index 115c2e9be..cdd51e2bb 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 3d7ec8e67..8cad7a8b2 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -2,11 +2,12 @@ name: Test Windows
on:
push:
+ branches:
+ - "**"
paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -14,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
@@ -32,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"]
+ python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30
@@ -56,25 +56,26 @@ jobs:
# sets env: pythonLocation
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
cache: pip
cache-dependency-path: ".github/workflows/test-windows.yml"
- name: Print build system information
run: python3 .github/workflows/system-info.py
- - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
- run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
+ - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
+ run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
- name: Install dependencies
id: install
run: |
- 7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\"
- echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH
+ choco install nasm --no-progress
+ echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
- choco install ghostscript --version=10.0.0.20230317
+ choco install ghostscript --version=10.0.0.20230317 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images
@@ -88,7 +89,7 @@ jobs:
- name: Cache build
id: build-cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: winbuild\build
key:
@@ -166,7 +167,6 @@ jobs:
- name: Build Pillow
run: |
$FLAGS="-C raqm=vendor -C fribidi=vendor"
- if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" }
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
& $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh
@@ -190,7 +190,7 @@ jobs:
shell: bash
- name: Upload errors
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: failure()
with:
name: errors
@@ -208,47 +208,6 @@ jobs:
flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }}
- - name: Build wheel
- id: wheel
- if: "github.event_name != 'pull_request'"
- run: |
- mkdir fribidi
- copy winbuild\build\bin\fribidi* fribidi
- setlocal EnableDelayedExpansion
- for %%f in (winbuild\build\license\*) do (
- set x=%%~nf
- rem Skip FriBiDi license, it is not included in the wheel.
- set fribidi=!x:~0,7!
- if NOT !fribidi!==fribidi (
- rem Skip imagequant license, it is not included in the wheel.
- set libimagequant=!x:~0,13!
- if NOT !libimagequant!==libimagequant (
- echo. >> LICENSE
- echo ===== %%~nf ===== >> LICENSE
- echo. >> LICENSE
- type %%f >> LICENSE
- )
- )
- )
- for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
- call winbuild\\build\\build_env.cmd
- %pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable .
- shell: cmd
-
- - name: Upload wheel
- uses: actions/upload-artifact@v3
- if: "github.event_name != 'pull_request'"
- with:
- name: ${{ steps.wheel.outputs.dist }}
- path: "*.whl"
-
- - name: Upload fribidi.dll
- if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
- uses: actions/upload-artifact@v3
- with:
- name: fribidi
- path: fribidi\*
-
success:
permissions:
contents: none
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 201f9ef77..ae84a4d8f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
@@ -28,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ FORCE_COLOR: 1
+
jobs:
build:
@@ -35,12 +36,13 @@ jobs:
fail-fast: false
matrix:
os: [
- "macos-latest",
+ "macos-14",
"ubuntu-latest",
]
python-version: [
"pypy3.10",
"pypy3.9",
+ "3.13",
"3.12",
"3.11",
"3.10",
@@ -48,11 +50,21 @@ jobs:
"3.8",
]
include:
- - python-version: "3.9"
+ - python-version: "3.11"
PYTHONOPTIMIZE: 1
REVERSE: "--reverse"
- - python-version: "3.8"
+ - python-version: "3.10"
PYTHONOPTIMIZE: 2
+ # M1 only available for 3.10+
+ - os: "macos-latest"
+ python-version: "3.9"
+ - os: "macos-latest"
+ python-version: "3.8"
+ exclude:
+ - os: "macos-14"
+ python-version: "3.9"
+ - os: "macos-14"
+ python-version: "3.8"
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
@@ -61,21 +73,33 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
cache: pip
- cache-dependency-path: ".ci/*.sh"
+ cache-dependency-path: |
+ ".ci/*.sh"
+ "pyproject.toml"
- name: Build system information
run: python3 .github/workflows/system-info.py
+ - name: Cache libimagequant
+ if: startsWith(matrix.os, 'ubuntu')
+ uses: actions/cache@v4
+ id: cache-libimagequant
+ with:
+ path: ~/cache-libimagequant
+ key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
+
- name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu')
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
+ GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
- name: Install macOS dependencies
if: startsWith(matrix.os, 'macOS')
@@ -84,6 +108,10 @@ jobs:
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
+ - name: Register gcc problem matcher
+ if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
+ run: echo "::add-matcher::.github/problem-matchers/gcc.json"
+
- name: Build
run: |
.ci/build.sh
@@ -110,7 +138,7 @@ jobs:
mkdir -p Tests/errors
- name: Upload errors
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: failure()
with:
name: errors
@@ -123,7 +151,7 @@ jobs:
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
- flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }}
+ flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true
diff --git a/.github/workflows/wheels-build.sh b/.github/workflows/wheels-build.sh
deleted file mode 100755
index 0aeec6b96..000000000
--- a/.github/workflows/wheels-build.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/bin/bash
-
-if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
- # webp, zstd, xz, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
- # libxdmcp causes an issue on macOS < 11
- # curl from brew requires zstd, use system curl
- # if php is installed, brew tries to reinstall these after installing openblas
- # remove lcms2 and libpng to fix building openjpeg on arm64
- brew remove --ignore-dependencies webp zstd xz libpng libtiff libxcb libxdmcp curl php lcms2 ghostscript
-
- brew install pkg-config
-
- if [[ "$PLAT" == "arm64" ]]; then
- export MACOSX_DEPLOYMENT_TARGET="11.0"
- else
- export MACOSX_DEPLOYMENT_TARGET="10.10"
- fi
-fi
-
-if [[ "$MB_PYTHON_VERSION" == pypy3* ]]; then
- MB_PYTHON_OSX_VER="10.9"
-fi
-
-echo "::group::Install a virtualenv"
- source wheels/multibuild/common_utils.sh
- source wheels/multibuild/travis_steps.sh
- python3 -m pip install virtualenv
- before_install
-echo "::endgroup::"
-
-echo "::group::Build wheel"
- build_wheel
- ls -l "${GITHUB_WORKSPACE}/${WHEEL_SDIR}/"
-echo "::endgroup::"
-
-if [[ $MACOSX_DEPLOYMENT_TARGET != "11.0" ]]; then
- echo "::group::Test wheel"
- install_run
- echo "::endgroup::"
-fi
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
new file mode 100755
index 000000000..3ec314873
--- /dev/null
+++ b/.github/workflows/wheels-dependencies.sh
@@ -0,0 +1,151 @@
+#!/bin/bash
+# Define custom utilities
+# Test for macOS with [ -n "$IS_MACOS" ]
+if [ -z "$IS_MACOS" ]; then
+ export MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
+ export MB_ML_VER=${AUDITWHEEL_POLICY:9}
+fi
+export PLAT=$CIBW_ARCHS
+source wheels/multibuild/common_utils.sh
+source wheels/multibuild/library_builders.sh
+if [ -z "$IS_MACOS" ]; then
+ source wheels/multibuild/manylinux_utils.sh
+fi
+
+ARCHIVE_SDIR=pillow-depends-main
+
+# Package versions for fresh source builds
+FREETYPE_VERSION=2.13.2
+HARFBUZZ_VERSION=8.3.0
+LIBPNG_VERSION=1.6.40
+JPEGTURBO_VERSION=3.0.1
+OPENJPEG_VERSION=2.5.0
+XZ_VERSION=5.4.5
+TIFF_VERSION=4.6.0
+LCMS2_VERSION=2.16
+if [[ -n "$IS_MACOS" ]]; then
+ GIFLIB_VERSION=5.1.4
+else
+ GIFLIB_VERSION=5.2.1
+fi
+if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
+ ZLIB_VERSION=1.3
+else
+ ZLIB_VERSION=1.2.8
+fi
+LIBWEBP_VERSION=1.3.2
+BZIP2_VERSION=1.0.8
+LIBXCB_VERSION=1.16
+BROTLI_VERSION=1.1.0
+
+if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
+ function build_openjpeg {
+ local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz)
+ (cd $out_dir \
+ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
+ && make install)
+ touch openjpeg-stamp
+ }
+fi
+
+function build_brotli {
+ local cmake=$(get_modern_cmake)
+ local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz)
+ (cd $out_dir \
+ && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
+ && make install)
+ if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
+ cp /usr/local/lib64/libbrotli* /usr/local/lib
+ cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig
+ fi
+}
+
+function build {
+ if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
+ export BUILD_PREFIX="/usr/local"
+ fi
+ build_xz
+ if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
+ yum remove -y zlib-devel
+ fi
+ build_new_zlib
+
+ build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
+ if [ -n "$IS_MACOS" ]; then
+ if [[ "$CIBW_ARCHS" == "arm64" ]]; then
+ build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
+ build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
+ build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
+ if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
+ cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
+ fi
+ fi
+ else
+ sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
+ fi
+ build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
+
+ build_libjpeg_turbo
+ build_tiff
+ build_libpng
+ build_lcms2
+ if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
+ for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do
+ cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib
+ done
+ fi
+ build_openjpeg
+
+ ORIGINAL_CFLAGS=$CFLAGS
+ CFLAGS="$CFLAGS -O3 -DNDEBUG"
+ if [[ -n "$IS_MACOS" ]]; then
+ CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
+ fi
+ build_libwebp
+ CFLAGS=$ORIGINAL_CFLAGS
+
+ build_brotli
+
+ if [ -n "$IS_MACOS" ]; then
+ # Custom freetype build
+ build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
+ else
+ build_freetype
+ fi
+
+ if [ -z "$IS_MACOS" ]; then
+ export FREETYPE_LIBS=-lfreetype
+ export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
+ fi
+ build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
+ if [ -z "$IS_MACOS" ]; then
+ export FREETYPE_LIBS=""
+ export FREETYPE_CFLAGS=""
+ fi
+}
+
+# Any stuff that you need to do before you start building the wheels
+# Runs in the root directory of this repository.
+curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
+untar pillow-depends-main.zip
+
+if [[ -n "$IS_MACOS" ]]; then
+ # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
+ # libxdmcp causes an issue on macOS < 11
+ # if php is installed, brew tries to reinstall these after installing openblas
+ # remove cairo to fix building harfbuzz on arm64
+ # remove lcms2 and libpng to fix building openjpeg on arm64
+ # remove zstd to avoid inclusion on x86_64
+ # curl from brew requires zstd, use system curl
+ brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd
+
+ brew install pkg-config
+fi
+
+wrap_wheel_builder build
+
+# Append licenses
+for filename in wheels/dependency_licenses/*; do
+ echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE
+ cat $filename >> LICENSE
+done
diff --git a/.github/workflows/wheels-linux.yml b/.github/workflows/wheels-linux.yml
deleted file mode 100644
index 8b2d9d451..000000000
--- a/.github/workflows/wheels-linux.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-name: Build Linux wheels
-
-on:
- workflow_call:
- inputs:
- artifacts-name:
- required: true
- type: string
-
-env:
- CONFIG_PATH: "wheels/config.sh"
- REPO_DIR: "."
- TEST_DEPENDS: "pytest pytest-timeout"
-
-jobs:
- build:
- name: ${{ matrix.python }} ${{ matrix.mb-ml-libc }}${{ matrix.mb-ml-ver }}
- runs-on: "ubuntu-latest"
- strategy:
- fail-fast: false
- matrix:
- python: [
- "pypy3.9-7.3.13",
- "pypy3.10-7.3.13",
- "3.8",
- "3.9",
- "3.10",
- "3.11",
- "3.12",
- ]
- mb-ml-libc: [ "manylinux" ]
- mb-ml-ver: [ 2014, "_2_28" ]
- include:
- - python: "3.8"
- mb-ml-libc: "musllinux"
- mb-ml-ver: "_1_1"
- - python: "3.9"
- mb-ml-libc: "musllinux"
- mb-ml-ver: "_1_1"
- - python: "3.10"
- mb-ml-libc: "musllinux"
- mb-ml-ver: "_1_1"
- - python: "3.11"
- mb-ml-libc: "musllinux"
- mb-ml-ver: "_1_1"
- - python: "3.12"
- mb-ml-libc: "musllinux"
- mb-ml-ver: "_1_1"
- env:
- MB_PYTHON_VERSION: ${{ matrix.python }}
- MB_ML_LIBC: ${{ matrix.mb-ml-libc }}
- MB_ML_VER: ${{ matrix.mb-ml-ver }}
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: true
- - uses: actions/setup-python@v4
- with:
- python-version: "3.x"
- - name: Build Wheel
- run: .github/workflows/wheels-build.sh
- - uses: actions/upload-artifact@v3
- with:
- name: ${{ inputs.artifacts-name }}
- path: wheelhouse/*.whl
- # Uncomment to get SSH access for testing
- # - name: Setup tmate session
- # if: failure()
- # uses: mxschmitt/action-tmate@v3
diff --git a/.github/workflows/wheels-macos.yml b/.github/workflows/wheels-macos.yml
deleted file mode 100644
index c51abf39a..000000000
--- a/.github/workflows/wheels-macos.yml
+++ /dev/null
@@ -1,57 +0,0 @@
-name: Build macOS wheels
-
-on:
- workflow_call:
- inputs:
- artifacts-name:
- required: true
- type: string
-
-env:
- CONFIG_PATH: "wheels/config.sh"
- REPO_DIR: "."
- TEST_DEPENDS: "pytest pytest-timeout"
-
-jobs:
- build:
- name: ${{ matrix.python }} ${{ matrix.platform }}
- runs-on: "macos-latest"
- strategy:
- fail-fast: false
- matrix:
- python: [
- "pypy3.9-7.3.13",
- "pypy3.10-7.3.13",
- "3.8",
- "3.9",
- "3.10",
- "3.11",
- "3.12",
- ]
- platform: [ "x86_64", "arm64" ]
- exclude:
- - python: "pypy3.9-7.3.13"
- platform: "arm64"
- - python: "pypy3.10-7.3.13"
- platform: "arm64"
- env:
- PLAT: ${{ matrix.platform }}
- MB_PYTHON_VERSION: ${{ matrix.python }}
- TRAVIS_OS_NAME: "osx"
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: true
- - uses: actions/setup-python@v4
- with:
- python-version: "3.x"
- - name: Build Wheel
- run: .github/workflows/wheels-build.sh
- - uses: actions/upload-artifact@v3
- with:
- name: ${{ inputs.artifacts-name }}
- path: wheelhouse/*.whl
- # Uncomment to get SSH access for testing
- # - name: Setup tmate session
- # if: failure()
- # uses: mxschmitt/action-tmate@v3
diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1
new file mode 100644
index 000000000..f593c7228
--- /dev/null
+++ b/.github/workflows/wheels-test.ps1
@@ -0,0 +1,22 @@
+param ([string]$venv, [string]$pillow="C:\pillow")
+$ErrorActionPreference = 'Stop'
+$ProgressPreference = 'SilentlyContinue'
+Set-PSDebug -Trace 1
+if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
+ # unlike CPython, PyPy requires Visual C++ Redistributable to be installed
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe'
+ C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
+}
+$env:path += ";$pillow\winbuild\build\bin\"
+& "$venv\Scripts\activate.ps1"
+& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
+cd $pillow
+& python -VV
+if (!$?) { exit $LASTEXITCODE }
+& python selftest.py
+if (!$?) { exit $LASTEXITCODE }
+& python -m pytest -vx Tests\check_wheel.py
+if (!$?) { exit $LASTEXITCODE }
+& python -m pytest -vx Tests
+if (!$?) { exit $LASTEXITCODE }
diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh
new file mode 100755
index 000000000..207ec1567
--- /dev/null
+++ b/.github/workflows/wheels-test.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+set -e
+
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ brew install fribidi
+ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
+elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
+ apk add curl fribidi
+else
+ yum install -y fribidi
+fi
+if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
+ python3 -m pip install numpy
+fi
+
+if [ ! -d "test-images-main" ]; then
+ curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
+ unzip pillow-test-images.zip
+ mv test-images-main/* Tests/images
+fi
+
+# Runs tests
+python3 selftest.py
+python3 -m pytest Tests/check_wheel.py
+python3 -m pytest
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 4381a9856..1140aaaad 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -3,14 +3,20 @@ name: Wheels
on:
push:
paths:
- - ".github/workflows/wheels*.yml"
+ - ".ci/requirements-cibw.txt"
+ - ".github/workflows/wheel*"
- "wheels/*"
+ - "winbuild/build_prepare.py"
+ - "winbuild/fribidi.cmake"
tags:
- "*"
pull_request:
paths:
- - ".github/workflows/wheels*.yml"
+ - ".ci/requirements-cibw.txt"
+ - ".github/workflows/wheel*"
- "wheels/*"
+ - "winbuild/build_prepare.py"
+ - "winbuild/fribidi.cmake"
workflow_dispatch:
permissions:
@@ -20,23 +26,239 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ FORCE_COLOR: 1
+
jobs:
- macos:
- uses: ./.github/workflows/wheels-macos.yml
- with:
- artifacts-name: "wheels"
-
- linux:
- uses: ./.github/workflows/wheels-linux.yml
- with:
- artifacts-name: "wheels"
-
- success:
- permissions:
- contents: none
- needs: [macos, linux]
+ build-1-QEMU-emulated-wheels:
+ name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest
- name: Wheels Successful
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version:
+ - pp39
+ - pp310
+ - cp38
+ - cp39
+ - cp310
+ - cp311
+ - cp312
+ spec:
+ - manylinux2014
+ - manylinux_2_28
+ - musllinux
+ exclude:
+ - { python-version: pp39, spec: musllinux }
+ - { python-version: pp310, spec: musllinux }
+
steps:
- - name: Success
- run: echo Wheels Successful
+ - uses: actions/checkout@v4
+ with:
+ submodules: true
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ # https://github.com/docker/setup-qemu-action
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Install cibuildwheel
+ run: |
+ python3 -m pip install -r .ci/requirements-cibw.txt
+
+ - name: Build wheels
+ run: |
+ python3 -m cibuildwheel --output-dir wheelhouse
+ env:
+ # Build only the currently selected Linux architecture (so we can
+ # parallelise for speed).
+ CIBW_ARCHS: "aarch64"
+ # Likewise, select only one Python version per job to speed this up.
+ CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
+ # Extra options for manylinux.
+ CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
+ CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }}
+ path: ./wheelhouse/*.whl
+
+ build-2-native-wheels:
+ name: ${{ matrix.name }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: "macOS x86_64"
+ os: macos-latest
+ cibw_arch: x86_64
+ macosx_deployment_target: "10.10"
+ - name: "macOS arm64"
+ os: macos-latest
+ cibw_arch: arm64
+ macosx_deployment_target: "11.0"
+ - name: "manylinux2014 and musllinux x86_64"
+ os: ubuntu-latest
+ cibw_arch: x86_64
+ - name: "manylinux_2_28 x86_64"
+ os: ubuntu-latest
+ cibw_arch: x86_64
+ build: "*manylinux*"
+ manylinux: "manylinux_2_28"
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: true
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ - name: Install cibuildwheel
+ run: |
+ python3 -m pip install -r .ci/requirements-cibw.txt
+
+ - name: Build wheels
+ run: |
+ python3 -m cibuildwheel --output-dir wheelhouse
+ env:
+ CIBW_ARCHS: ${{ matrix.cibw_arch }}
+ CIBW_BUILD: ${{ matrix.build }}
+ CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
+ CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
+ CIBW_SKIP: pp38-*
+ CIBW_TEST_SKIP: "*-macosx_arm64"
+ MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
+ path: ./wheelhouse/*.whl
+
+ windows:
+ name: Windows ${{ matrix.cibw_arch }}
+ runs-on: windows-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - cibw_arch: x86
+ - cibw_arch: AMD64
+ - cibw_arch: ARM64
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Checkout extra test images
+ uses: actions/checkout@v4
+ with:
+ repository: python-pillow/test-images
+ path: Tests\test-images
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ - name: Install cibuildwheel
+ run: |
+ python.exe -m pip install -r .ci/requirements-cibw.txt
+
+ - name: Prepare for build
+ run: |
+ choco install nasm --no-progress
+ echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
+
+ # 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 }}
+ shell: pwsh
+
+ - name: Build wheels
+ run: |
+ setlocal EnableDelayedExpansion
+ for %%f in (winbuild\build\license\*) do (
+ set x=%%~nf
+ rem Skip FriBiDi license, it is not included in the wheel.
+ set fribidi=!x:~0,7!
+ if NOT !fribidi!==fribidi (
+ rem Skip imagequant license, it is not included in the wheel.
+ set libimagequant=!x:~0,13!
+ if NOT !libimagequant!==libimagequant (
+ echo. >> LICENSE
+ echo ===== %%~nf ===== >> LICENSE
+ echo. >> LICENSE
+ type %%f >> LICENSE
+ )
+ )
+ )
+ call winbuild\\build\\build_env.cmd
+ %pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
+ env:
+ CIBW_ARCHS: ${{ matrix.cibw_arch }}
+ CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
+ CIBW_CACHE_PATH: "C:\\cibw"
+ CIBW_SKIP: pp38-*
+ CIBW_TEST_SKIP: "*-win_arm64"
+ CIBW_TEST_COMMAND: 'docker run --rm
+ -v {project}:C:\pillow
+ -v C:\cibw:C:\cibw
+ -v %CD%\..\venv-test:%CD%\..\venv-test
+ -e CI -e GITHUB_ACTIONS
+ mcr.microsoft.com/windows/servercore:ltsc2022
+ powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
+ shell: cmd
+
+ - name: Upload wheels
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist-windows-${{ matrix.cibw_arch }}
+ path: ./wheelhouse/*.whl
+
+ - name: Upload fribidi.dll
+ uses: actions/upload-artifact@v4
+ with:
+ name: fribidi-windows-${{ matrix.cibw_arch }}
+ path: winbuild\build\bin\fribidi*
+
+ sdist:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+ cache: pip
+ cache-dependency-path: "Makefile"
+
+ - run: make sdist
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dist-sdist
+ path: dist/*.tar.gz
+
+ pypi-publish:
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+ needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
+ runs-on: ubuntu-latest
+ name: Upload release to PyPI
+ environment:
+ name: release-pypi
+ url: https://pypi.org/p/Pillow
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ pattern: dist-*
+ path: dist
+ merge-multiple: true
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a8c7696df..5ce0c9a17 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,56 +1,38 @@
repos:
- - repo: https://github.com/asottile/pyupgrade
- rev: v3.13.0
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.1.9
hooks:
- - id: pyupgrade
- args: [--py38-plus]
+ - id: ruff
+ args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
- rev: 23.9.1
+ rev: 23.12.1
hooks:
- id: black
- args: [--target-version=py38]
-
- - repo: https://github.com/PyCQA/isort
- rev: 5.12.0
- hooks:
- - id: isort
- repo: https://github.com/PyCQA/bandit
- rev: 1.7.5
+ rev: 1.7.6
hooks:
- id: bandit
args: [--severity-level=high]
files: ^src/
- - repo: https://github.com/asottile/yesqa
- rev: v1.5.0
- hooks:
- - id: yesqa
-
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- - repo: https://github.com/PyCQA/flake8
- rev: 6.1.0
- hooks:
- - id: flake8
- additional_dependencies:
- [flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
-
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- - id: python-check-blanket-noqa
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.5.0
hooks:
- id: check-executables-have-shebangs
+ - id: check-shebang-scripts-are-executable
- id: check-merge-conflict
- id: check-json
- id: check-toml
@@ -61,17 +43,17 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/sphinx-contrib/sphinx-lint
- rev: v0.6.8
+ rev: v0.9.1
hooks:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: 1.2.0
+ rev: 1.5.3
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
- rev: v0.14
+ rev: v0.15
hooks:
- id: validate-pyproject
diff --git a/.readthedocs.yml b/.readthedocs.yml
index bda03d944..0c8f935d5 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -5,7 +5,7 @@ formats: [pdf]
build:
os: ubuntu-22.04
tools:
- python: "3.11"
+ python: "3"
python:
install:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index e4584ec88..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,135 +0,0 @@
-if: tag IS present OR type = api
-
-env:
- global:
- - CONFIG_PATH=wheels/config.sh
- - REPO_DIR=.
- - PLAT=aarch64
- - TEST_DEPENDS=pytest-timeout
-
-language: python
-# Default Python version is usually 3.6
-python: "3.11"
-dist: focal
-services: docker
-
-jobs:
- include:
- - name: "3.8 Focal manylinux2014 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER=2014
- - MB_PYTHON_VERSION=3.8
- - name: "3.8 Focal manylinux_2_28 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_2_28"
- - MB_PYTHON_VERSION=3.8
- - name: "3.8 musllinux_1_1 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_1_1"
- - MB_ML_LIBC="musllinux"
- - MB_PYTHON_VERSION=3.8
- - name: "3.9 Focal manylinux2014 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER=2014
- - MB_PYTHON_VERSION=3.9
- - name: "3.9 Focal manylinux_2_28 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_2_28"
- - MB_PYTHON_VERSION=3.9
- - name: "3.9 musllinux_1_1 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_1_1"
- - MB_ML_LIBC="musllinux"
- - MB_PYTHON_VERSION=3.9
- - name: "3.10 Focal manylinux2014 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER=2014
- - MB_PYTHON_VERSION=3.10
- - name: "3.10 Focal manylinux_2_28 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_2_28"
- - MB_PYTHON_VERSION=3.10
- - name: "3.10 musllinux_1_1 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_1_1"
- - MB_ML_LIBC="musllinux"
- - MB_PYTHON_VERSION=3.10
- - name: "3.11 Focal manylinux_2_28 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER=2014
- - MB_PYTHON_VERSION=3.11
- - name: "3.11 Focal manylinux_2_28 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_2_28"
- - MB_PYTHON_VERSION=3.11
- - name: "3.11 musllinux_1_1 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_1_1"
- - MB_ML_LIBC="musllinux"
- - MB_PYTHON_VERSION=3.11
- - name: "3.12 Focal manylinux_2_28 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER=2014
- - MB_PYTHON_VERSION=3.12
- - name: "3.12 Focal manylinux_2_28 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_2_28"
- - MB_PYTHON_VERSION=3.12
- - name: "3.12 musllinux_1_1 aarch64"
- os: linux
- arch: arm64
- env:
- - MB_ML_VER="_1_1"
- - MB_ML_LIBC="musllinux"
- - MB_PYTHON_VERSION=3.12
-
-before_install:
- - source wheels/multibuild/common_utils.sh
- - source wheels/multibuild/travis_steps.sh
- - before_install
-
-install:
- - build_multilinux aarch64 build_wheel
- - ls -l "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/"
-
-script:
- - install_run
-
-# Upload wheels to GitHub Releases
-deploy:
- provider: releases
- api_key: $GITHUB_RELEASE_TOKEN
- file_glob: true
- file: "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/*.whl"
- on:
- repo: python-pillow/Pillow
- tags: true
- skip_cleanup: true
diff --git a/CHANGES.rst b/CHANGES.rst
index d2f2bb462..7d80eec03 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,141 @@
Changelog (Pillow)
==================
+10.3.0 (unreleased)
+-------------------
+
+- Do not support using test-image-results to upload images after test failures #7739
+ [radarhere]
+
+- Changed ImageMath.ops to be static #7721
+ [radarhere]
+
+- Fix APNG info after seeking backwards more than twice #7701
+ [esoma, radarhere]
+
+- Deprecate ImageCms constants and versions() function #7702
+ [nulano, radarhere]
+
+- Added PerspectiveTransform #7699
+ [radarhere]
+
+- Add support for reading and writing grayscale PFM images #7696
+ [nulano, hugovk]
+
+- Add LCMS2 flags to ImageCms #7676
+ [nulano, radarhere, hugovk]
+
+- Rename x64 to AMD64 in winbuild #7693
+ [nulano]
+
+10.2.0 (2024-01-02)
+-------------------
+
+- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553
+ [bgilbert, radarhere]
+
+- Trim glyph size in ImageFont.getmask() #7669, #7672
+ [radarhere, nulano]
+
+- Deprecate IptcImagePlugin helpers #7664
+ [nulano, hugovk, radarhere]
+
+- Allow uncompressed TIFF images to be saved in chunks #7650
+ [radarhere]
+
+- Concatenate multiple JPEG EXIF markers #7496
+ [radarhere]
+
+- Changed IPTC tile tuple to match other plugins #7661
+ [radarhere]
+
+- Do not assign new fp attribute when exiting context manager #7566
+ [radarhere]
+
+- Support arbitrary masks for uncompressed RGB DDS images #7589
+ [radarhere, akx]
+
+- Support setting ROWSPERSTRIP tag #7654
+ [radarhere]
+
+- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662
+ [radarhere]
+
+- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657
+ [hugovk]
+
+- Restricted environment keys for ImageMath.eval() #7655
+ [wiredfool, radarhere]
+
+- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641
+ [hugovk, radarhere]
+
+- Fix incorrect color blending for overlapping glyphs #7497
+ [ZachNagengast, nulano, radarhere]
+
+- Attempt memory mapping when tile args is a string #7565
+ [radarhere]
+
+- Fill identical pixels with transparency in subsequent frames when saving GIF #7568
+ [radarhere]
+
+- Corrected duration when combining multiple GIF frames into single frame #7521
+ [radarhere]
+
+- Handle disposing GIF background from outside palette #7515
+ [radarhere]
+
+- Seek past the data when skipping a PSD layer #7483
+ [radarhere]
+
+- Import plugins relative to the module #7576
+ [deliangyang, jaxx0n]
+
+- Translate encoder error codes to strings; deprecate ``ImageFile.raise_oserror()`` #7609
+ [bgilbert, radarhere]
+
+- Support reading BC4U and DX10 BC1 images #6486
+ [REDxEYE, radarhere, hugovk]
+
+- Optimize ImageStat.Stat.extrema #7593
+ [florath, radarhere]
+
+- Handle pathlib.Path in FreeTypeFont #7578
+ [radarhere, hugovk, nulano]
+
+- Added support for reading DX10 BC4 DDS images #7603
+ [sambvfx, radarhere]
+
+- Optimized ImageStat.Stat.count #7599
+ [florath]
+
+- Correct PDF palette size when saving #7555
+ [radarhere]
+
+- Fixed closing file pointer with olefile 0.47 #7594
+ [radarhere]
+
+- Raise ValueError when TrueType font size is not greater than zero #7584, #7587
+ [akx, radarhere]
+
+- If absent, do not try to close fp when closing image #7557
+ [RaphaelVRossi, radarhere]
+
+- Allow configuring JPEG restart marker interval on save #7488
+ [bgilbert, radarhere]
+
+- Decrement reference count for PyObject #7549
+ [radarhere]
+
+- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491
+ [bgilbert, radarhere]
+
+- If save_all PNG only has one frame, do not create animated image #7522
+ [radarhere]
+
+- Fixed frombytes() for images with a zero dimension #7493
+ [radarhere]
+
10.1.0 (2023-10-15)
-------------------
@@ -2191,7 +2326,7 @@ Changelog (Pillow)
- Cache EXIF information #3498
[Glandos]
-- Added transparency for all PNG greyscale modes #3744
+- Added transparency for all PNG grayscale modes #3744
[radarhere]
- Fix deprecation warnings in Python 3.8 #3749
@@ -4693,7 +4828,7 @@ Changelog (Pillow)
- Fix Bicubic interpolation #970
[homm]
-- Support for 4-bit greyscale TIFF images #980
+- Support for 4-bit grayscale TIFF images #980
[hugovk]
- Updated manifest #957
@@ -6768,7 +6903,7 @@ The test suite includes 750 individual tests.
- You can now convert directly between all modes supported by
PIL. When converting colour images to "P", PIL defaults to
- a "web" palette and dithering. When converting greyscale
+ a "web" palette and dithering. When converting grayscale
images to "1", PIL uses a thresholding and dithering.
- Added a "dither" option to "convert". By default, "convert"
@@ -6846,13 +6981,13 @@ The test suite includes 530 individual tests.
- Fixed "paste" to allow a mask also for mode "F" images.
- The BMP driver now saves mode "1" images. When loading images, the mode
- is set to "L" for 8-bit files with greyscale palettes, and to "P" for
+ is set to "L" for 8-bit files with grayscale palettes, and to "P" for
other 8-bit files.
- The IM driver now reads and saves "1" images (file modes "0 1" or "L 1").
- The JPEG and GIF drivers now saves "1" images. For JPEG, the image
- is saved as 8-bit greyscale (it will load as mode "L"). For GIF, the
+ is saved as 8-bit grayscale (it will load as mode "L"). For GIF, the
image will be loaded as a "P" image.
- Fixed a potential buffer overrun in the GIF encoder.
@@ -7156,7 +7291,7 @@ The test suite includes 400 individual tests.
drawing capabilities can be used to render vector and metafile
formats.
-- Added restricted drivers for images from Image Tools (greyscale
+- Added restricted drivers for images from Image Tools (grayscale
only) and LabEye/IFUNC (common interchange modes only).
- Some minor improvements to the sample scripts provided in the
diff --git a/LICENSE b/LICENSE
index cf65e86d7..0069eb5bc 100644
--- a/LICENSE
+++ b/LICENSE
@@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
- Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors.
+ Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
Like PIL, Pillow is licensed under the open source HPND License:
diff --git a/MANIFEST.in b/MANIFEST.in
index 2bbddefa3..af25dfd2d 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -5,8 +5,10 @@ include *.md
include *.py
include *.rst
include *.sh
+include *.toml
include *.txt
include *.yaml
+include .flake8
include LICENSE
include Makefile
include tox.ini
diff --git a/Makefile b/Makefile
index 57d756b47..ad0a1adab 100644
--- a/Makefile
+++ b/Makefile
@@ -49,7 +49,7 @@ help:
@echo " install make and install"
@echo " install-coverage make and install with C coverage"
@echo " lint run the lint checks"
- @echo " lint-fix run Black and isort to (mostly) fix lint issues"
+ @echo " lint-fix run Ruff to (mostly) fix lint issues"
@echo " release-test run code and package tests before release"
@echo " test run tests on installed Pillow"
@@ -118,6 +118,6 @@ lint:
.PHONY: lint-fix
lint-fix:
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
- python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
- python3 -m black --target-version py38 .
- python3 -m isort .
+ python3 -m black .
+ python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
+ python3 -m ruff --fix .
diff --git a/README.md b/README.md
index e11bd2faa..6ca870166 100644
--- a/README.md
+++ b/README.md
@@ -48,9 +48,6 @@ As of 2019, Pillow development is
-
@@ -68,10 +65,10 @@ As of 2019, Pillow development is
- ![]()
- ![]()
None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)]
-def iterate_set(size, access):
+def iterate_set(size, access) -> None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)] = (x % 256, y % 256, 0)
-def timer(func, label, *args):
+def timer(func, label, *args) -> None:
iterations = 5000
starttime = time.time()
for x in range(iterations):
@@ -36,7 +38,7 @@ def timer(func, label, *args):
)
-def test_direct():
+def test_direct() -> None:
im = hopper()
im.load()
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
@@ -45,7 +47,7 @@ def test_direct():
assert caccess[(0, 0)] == access[(0, 0)]
- print("Size: %sx%s" % im.size)
+ print(f"Size: {im.width}x{im.height}")
timer(iterate_get, "PyAccess - get", im.size, access)
timer(iterate_set, "PyAccess - set", im.size, access)
timer(iterate_get, "C-api - get", im.size, caccess)
diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py
index 7b3d4d7ee..e0057a2c2 100644
--- a/Tests/check_fli_oob.py
+++ b/Tests/check_fli_oob.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+from __future__ import annotations
from PIL import Image
diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py
index c600c45ed..5c89efc76 100644
--- a/Tests/check_fli_overflow.py
+++ b/Tests/check_fli_overflow.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
from PIL import Image
TEST_FILE = "Tests/images/fli_overflow.fli"
-def test_fli_overflow():
+def test_fli_overflow() -> None:
# this should not crash with a malloc error or access violation
with Image.open(TEST_FILE) as im:
im.load()
diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py
index a34bee45c..ac6be4869 100644
--- a/Tests/check_icns_dos.py
+++ b/Tests/check_icns_dos.py
@@ -1,5 +1,6 @@
# Tests potential DOS of IcnsImagePlugin with 0 length block.
# Run from anywhere that PIL is importable.
+from __future__ import annotations
from io import BytesIO
diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py
index d07082aba..890167039 100755
--- a/Tests/check_imaging_leaks.py
+++ b/Tests/check_imaging_leaks.py
@@ -1,4 +1,8 @@
#!/usr/bin/env python3
+from __future__ import annotations
+
+from typing import Any, Callable
+
import pytest
from PIL import Image
@@ -11,31 +15,34 @@ max_iterations = 10000
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
-def _get_mem_usage():
+def _get_mem_usage() -> float:
from resource import RUSAGE_SELF, getpagesize, getrusage
mem = getrusage(RUSAGE_SELF).ru_maxrss
return mem * getpagesize() / 1024 / 1024
-def _test_leak(min_iterations, max_iterations, fn, *args, **kwargs):
+def _test_leak(
+ min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any
+) -> None:
mem_limit = None
for i in range(max_iterations):
- fn(*args, **kwargs)
+ fn(*args)
mem = _get_mem_usage()
if i < min_iterations:
mem_limit = mem + 1
continue
msg = f"memory usage limit exceeded after {i + 1} iterations"
+ assert mem_limit is not None
assert mem <= mem_limit, msg
-def test_leak_putdata():
+def test_leak_putdata() -> None:
im = Image.new("RGB", (25, 25))
_test_leak(min_iterations, max_iterations, im.putdata, im.getdata())
-def test_leak_getlist():
+def test_leak_getlist() -> None:
im = Image.new("P", (25, 25))
_test_leak(
min_iterations,
diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py
index 71dcea4f3..2c63c3402 100644
--- a/Tests/check_j2k_dos.py
+++ b/Tests/check_j2k_dos.py
@@ -1,5 +1,6 @@
# Tests potential DOS of Jpeg2kImagePlugin with 0 length block.
# Run from anywhere that PIL is importable.
+from __future__ import annotations
from io import BytesIO
diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py
index afe5836f3..bbe35b591 100644
--- a/Tests/check_j2k_leaks.py
+++ b/Tests/check_j2k_leaks.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -18,7 +20,7 @@ pytestmark = [
]
-def test_leak_load():
+def test_leak_load() -> None:
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
@@ -28,7 +30,7 @@ def test_leak_load():
im.load()
-def test_leak_save():
+def test_leak_save() -> None:
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py
index b16412898..dbdd5a4f5 100644
--- a/Tests/check_j2k_overflow.py
+++ b/Tests/check_j2k_overflow.py
@@ -1,9 +1,13 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
-def test_j2k_overflow(tmp_path):
+def test_j2k_overflow(tmp_path: Path) -> None:
im = Image.new("RGBA", (1024, 131584))
target = str(tmp_path / "temp.jpc")
with pytest.raises(OSError):
diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py
old mode 100755
new mode 100644
index 0210505f5..954d68bf7
--- a/Tests/check_jp2_overflow.py
+++ b/Tests/check_jp2_overflow.py
@@ -1,5 +1,3 @@
-#!/usr/bin/env python3
-
# Reproductions/tests for OOB read errors in FliDecode.c
# When run in python, all of these images should fail for
@@ -12,7 +10,7 @@
# the output should be empty. There may be python issues
# in the valgrind especially if run in a debug python
# version.
-
+from __future__ import annotations
from PIL import Image
diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py
index 940c0b00d..5f290c6cd 100644
--- a/Tests/check_jpeg_leaks.py
+++ b/Tests/check_jpeg_leaks.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -109,14 +111,14 @@ standard_chrominance_qtable = (
[standard_l_qtable, standard_chrominance_qtable],
),
)
-def test_qtables_leak(qtables):
+def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
im = hopper("RGB")
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables)
-def test_exif_leak():
+def test_exif_leak() -> None:
"""
pre patch:
@@ -179,7 +181,7 @@ def test_exif_leak():
im.save(test_output, "JPEG", exif=exif)
-def test_base_save():
+def test_base_save() -> None:
"""
base case:
MB
diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py
index d98f4a694..a9ce79e57 100644
--- a/Tests/check_large_memory.py
+++ b/Tests/check_large_memory.py
@@ -1,4 +1,8 @@
+from __future__ import annotations
+
import sys
+from pathlib import Path
+from types import ModuleType
import pytest
@@ -14,6 +18,7 @@ from PIL import Image
# 2.7 and 3.2.
+numpy: ModuleType | None
try:
import numpy
except ImportError:
@@ -26,23 +31,24 @@ XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
-def _write_png(tmp_path, xdim, ydim):
+def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
f = str(tmp_path / "temp.png")
im = Image.new("L", (xdim, ydim), 0)
im.save(f)
-def test_large(tmp_path):
+def test_large(tmp_path: Path) -> None:
"""succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM)
-def test_2gpx(tmp_path):
+def test_2gpx(tmp_path: Path) -> None:
"""failed prepatch"""
_write_png(tmp_path, XDIM, XDIM)
@pytest.mark.skipif(numpy is None, reason="Numpy is not installed")
-def test_size_greater_than_int():
+def test_size_greater_than_int() -> None:
+ assert numpy is not None
arr = numpy.ndarray(shape=(16394, 16394))
Image.fromarray(arr)
diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py
index 24cb1f722..f4ca8d0aa 100644
--- a/Tests/check_large_memory_numpy.py
+++ b/Tests/check_large_memory_numpy.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
import sys
+from pathlib import Path
import pytest
@@ -22,7 +25,7 @@ XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
-def _write_png(tmp_path, xdim, ydim):
+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")
@@ -30,11 +33,11 @@ def _write_png(tmp_path, xdim, ydim):
im.save(f)
-def test_large(tmp_path):
+def test_large(tmp_path: Path) -> None:
"""succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM)
-def test_2gpx(tmp_path):
+def test_2gpx(tmp_path: Path) -> None:
"""failed prepatch"""
_write_png(tmp_path, XDIM, XDIM)
diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py
index bd7f407e4..84bda53ed 100644
--- a/Tests/check_libtiff_segfault.py
+++ b/Tests/check_libtiff_segfault.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -5,7 +7,7 @@ from PIL import Image
TEST_FILE = "Tests/images/libtiff_segfault.tif"
-def test_libtiff_segfault():
+def test_libtiff_segfault() -> None:
"""This test should not segfault. It will on Pillow <= 3.1.0 and
libtiff >= 4.0.0
"""
diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py
index f4a129f50..d65ba6abc 100644
--- a/Tests/check_png_dos.py
+++ b/Tests/check_png_dos.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import zlib
from io import BytesIO
@@ -6,7 +8,7 @@ from PIL import Image, ImageFile, PngImagePlugin
TEST_FILE = "Tests/images/png_decompression_dos.png"
-def test_ignore_dos_text():
+def test_ignore_dos_text() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
@@ -22,7 +24,7 @@ def test_ignore_dos_text():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
-def test_dos_text():
+def test_dos_text() -> None:
try:
im = Image.open(TEST_FILE)
im.load()
@@ -34,7 +36,7 @@ def test_dos_text():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
-def test_dos_total_memory():
+def test_dos_total_memory() -> None:
im = Image.new("L", (1, 1))
compressed_data = zlib.compress(b"a" * 1024 * 1023)
@@ -51,7 +53,7 @@ def test_dos_total_memory():
try:
im2 = Image.open(b)
except ValueError as msg:
- assert "Too much memory" in msg
+ assert "Too much memory" in str(msg)
return
total_len = 0
diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py
index 0a9a898d7..cf414d7ff 100644
--- a/Tests/check_release_notes.py
+++ b/Tests/check_release_notes.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import sys
from pathlib import Path
diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py
new file mode 100644
index 000000000..4b91984f5
--- /dev/null
+++ b/Tests/check_wheel.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+import sys
+
+from PIL import features
+
+
+def test_wheel_modules() -> None:
+ expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
+
+ # tkinter is not available in cibuildwheel installed CPython on Windows
+ try:
+ import tkinter
+
+ assert tkinter
+ except ImportError:
+ expected_modules.remove("tkinter")
+
+ assert set(features.get_supported_modules()) == expected_modules
+
+
+def test_wheel_codecs() -> None:
+ expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
+
+ assert set(features.get_supported_codecs()) == expected_codecs
+
+
+def test_wheel_features() -> None:
+ expected_features = {
+ "webp_anim",
+ "webp_mux",
+ "transp_webp",
+ "raqm",
+ "fribidi",
+ "harfbuzz",
+ "libjpeg_turbo",
+ "xcb",
+ }
+
+ if sys.platform == "win32":
+ expected_features.remove("xcb")
+
+ assert set(features.get_supported_features()) == expected_features
diff --git a/Tests/conftest.py b/Tests/conftest.py
index 66da7593c..e00d1f019 100644
--- a/Tests/conftest.py
+++ b/Tests/conftest.py
@@ -1,7 +1,11 @@
+from __future__ import annotations
+
import io
+import pytest
-def pytest_report_header(config):
+
+def pytest_report_header(config: pytest.Config) -> str:
try:
from PIL import features
@@ -12,7 +16,7 @@ def pytest_report_header(config):
return f"pytest_report_header failed: {e}"
-def pytest_configure(config):
+def pytest_configure(config: pytest.Config) -> None:
config.addinivalue_line(
"markers",
"pil_noop_mark: A conditional mark where nothing special happens",
diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py
index e318eb732..41c76f87e 100755
--- a/Tests/createfontdatachunk.py
+++ b/Tests/createfontdatachunk.py
@@ -1,4 +1,6 @@
#!/usr/bin/env python3
+from __future__ import annotations
+
import base64
import os
diff --git a/Tests/fonts/CBDTTestFont.ttf b/Tests/fonts/CBDTTestFont.ttf
new file mode 100644
index 000000000..73444e8dc
Binary files /dev/null and b/Tests/fonts/CBDTTestFont.ttf differ
diff --git a/Tests/fonts/EBDTTestFont.ttf b/Tests/fonts/EBDTTestFont.ttf
new file mode 100644
index 000000000..046e9e45c
Binary files /dev/null and b/Tests/fonts/EBDTTestFont.ttf differ
diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt
index da559b3d3..3c8a23197 100644
--- a/Tests/fonts/LICENSE.txt
+++ b/Tests/fonts/LICENSE.txt
@@ -2,7 +2,6 @@
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
-NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
@@ -25,3 +24,5 @@ FreeMono.ttf is licensed under GPLv3.
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
"Public domain font. Share and enjoy."
+
+CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain.
diff --git a/Tests/fonts/NotoColorEmoji.ttf b/Tests/fonts/NotoColorEmoji.ttf
deleted file mode 100644
index ef7b72575..000000000
Binary files a/Tests/fonts/NotoColorEmoji.ttf and /dev/null differ
diff --git a/Tests/helper.py b/Tests/helper.py
index de5468d84..b2e7d43dd 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -1,14 +1,17 @@
"""
Helper functions.
"""
+from __future__ import annotations
import logging
import os
import shutil
+import subprocess
import sys
import sysconfig
import tempfile
from io import BytesIO
+from typing import Any, Callable, Sequence
import pytest
from packaging.version import parse as parse_version
@@ -17,42 +20,31 @@ from PIL import Image, ImageMath, features
logger = logging.getLogger(__name__)
-
-HAS_UPLOADER = False
-
+uploader = None
if os.environ.get("SHOW_ERRORS"):
- # local img.show for errors.
- HAS_UPLOADER = True
-
- class test_image_results:
- @staticmethod
- def upload(a, b):
- a.show()
- b.show()
-
+ uploader = "show"
elif "GITHUB_ACTIONS" in os.environ:
- HAS_UPLOADER = True
-
- class test_image_results:
- @staticmethod
- def upload(a, b):
- dir_errors = os.path.join(os.path.dirname(__file__), "errors")
- os.makedirs(dir_errors, exist_ok=True)
- tmpdir = tempfile.mkdtemp(dir=dir_errors)
- a.save(os.path.join(tmpdir, "a.png"))
- b.save(os.path.join(tmpdir, "b.png"))
- return tmpdir
-
-else:
- try:
- import test_image_results
-
- HAS_UPLOADER = True
- except ImportError:
- pass
+ uploader = "github_actions"
-def convert_to_comparable(a, b):
+def upload(a: Image.Image, b: Image.Image) -> str | None:
+ if uploader == "show":
+ # local img.show for errors.
+ a.show()
+ b.show()
+ elif uploader == "github_actions":
+ dir_errors = os.path.join(os.path.dirname(__file__), "errors")
+ os.makedirs(dir_errors, exist_ok=True)
+ tmpdir = tempfile.mkdtemp(dir=dir_errors)
+ a.save(os.path.join(tmpdir, "a.png"))
+ b.save(os.path.join(tmpdir, "b.png"))
+ return tmpdir
+ return None
+
+
+def convert_to_comparable(
+ a: Image.Image, b: Image.Image
+) -> tuple[Image.Image, Image.Image]:
new_a, new_b = a, b
if a.mode == "P":
new_a = Image.new("L", a.size)
@@ -65,14 +57,18 @@ def convert_to_comparable(a, b):
return new_a, new_b
-def assert_deep_equal(a, b, msg=None):
+def assert_deep_equal(
+ a: Sequence[Any], b: Sequence[Any], msg: str | None = None
+) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:
assert a == b, msg
-def assert_image(im, mode, size, msg=None):
+def assert_image(
+ im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None
+) -> None:
if mode is not None:
assert im.mode == mode, (
msg or f"got mode {repr(im.mode)}, expected {repr(mode)}"
@@ -84,28 +80,32 @@ def assert_image(im, mode, size, msg=None):
)
-def assert_image_equal(a, b, msg=None):
+def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
if a.tobytes() != b.tobytes():
- if HAS_UPLOADER:
- try:
- url = test_image_results.upload(a, b)
+ try:
+ url = upload(a, b)
+ if url:
logger.error("URL for test images: %s", url)
- except Exception:
- pass
+ except Exception:
+ pass
- assert False, msg or "got different content"
+ pytest.fail(msg or "got different content")
-def assert_image_equal_tofile(a, filename, msg=None, mode=None):
+def assert_image_equal_tofile(
+ a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
+) -> None:
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_equal(a, img, msg)
-def assert_image_similar(a, b, epsilon, msg=None):
+def assert_image_similar(
+ a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None
+) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
@@ -123,55 +123,68 @@ def assert_image_similar(a, b, epsilon, msg=None):
+ f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}"
)
except Exception as e:
- if HAS_UPLOADER:
- try:
- url = test_image_results.upload(a, b)
+ try:
+ url = upload(a, b)
+ if url:
logger.exception("URL for test images: %s", url)
- except Exception:
- pass
+ except Exception:
+ pass
raise e
-def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None):
+def assert_image_similar_tofile(
+ a: Image.Image,
+ filename: str,
+ epsilon: float,
+ msg: str | None = None,
+ mode: str | None = None,
+) -> None:
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg)
-def assert_all_same(items, msg=None):
+def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) == len(items), msg
-def assert_not_all_same(items, msg=None):
+def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) != len(items), msg
-def assert_tuple_approx_equal(actuals, targets, threshold, msg):
+def assert_tuple_approx_equal(
+ actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str
+) -> None:
"""Tests if actuals has values within threshold from targets"""
- value = True
for i, target in enumerate(targets):
- value *= target - threshold <= actuals[i] <= target + threshold
-
- assert value, msg + ": " + repr(actuals) + " != " + repr(targets)
+ if not (target - threshold <= actuals[i] <= target + threshold):
+ pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
-def skip_unless_feature(feature):
+def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
reason = f"{feature} not available"
return pytest.mark.skipif(not features.check(feature), reason=reason)
-def skip_unless_feature_version(feature, version_required, reason=None):
+def skip_unless_feature_version(
+ feature: str, required: str, reason: str | None = None
+) -> pytest.MarkDecorator:
if not features.check(feature):
return pytest.mark.skip(f"{feature} not available")
if reason is None:
- reason = f"{feature} is older than {version_required}"
- version_required = parse_version(version_required)
+ reason = f"{feature} is older than {required}"
+ version_required = parse_version(required)
version_available = parse_version(features.version(feature))
return pytest.mark.skipif(version_available < version_required, reason=reason)
-def mark_if_feature_version(mark, feature, version_blacklist, reason=None):
+def mark_if_feature_version(
+ mark: pytest.MarkDecorator,
+ feature: str,
+ version_blacklist: str,
+ reason: str | None = None,
+) -> pytest.MarkDecorator:
if not features.check(feature):
return pytest.mark.pil_noop_mark()
if reason is None:
@@ -192,7 +205,7 @@ class PillowLeakTestCase:
iterations = 100 # count
mem_limit = 512 # k
- def _get_mem_usage(self):
+ def _get_mem_usage(self) -> float:
"""
Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
between macOS and Linux rss reporting
@@ -214,7 +227,7 @@ class PillowLeakTestCase:
# This is the maximum resident set size used (in kilobytes).
return mem # Kb
- def _test_leak(self, core):
+ def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage()
for cycle in range(self.iterations):
core()
@@ -226,17 +239,17 @@ class PillowLeakTestCase:
# helpers
-def fromstring(data):
+def fromstring(data: bytes) -> Image.Image:
return Image.open(BytesIO(data))
-def tostring(im, string_format, **options):
+def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes:
out = BytesIO()
im.save(out, string_format, **options)
return out.getvalue()
-def hopper(mode=None, cache={}):
+def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
if mode is None:
# Always return fresh not-yet-loaded version of image.
# Operations on not-yet-loaded images is separate class of errors
@@ -257,19 +270,31 @@ def hopper(mode=None, cache={}):
return im.copy()
-def djpeg_available():
- return bool(shutil.which("djpeg"))
+def djpeg_available() -> bool:
+ if shutil.which("djpeg"):
+ try:
+ subprocess.check_call(["djpeg", "-version"])
+ return True
+ except subprocess.CalledProcessError: # pragma: no cover
+ return False
+ return False
-def cjpeg_available():
- return bool(shutil.which("cjpeg"))
+def cjpeg_available() -> bool:
+ if shutil.which("cjpeg"):
+ try:
+ subprocess.check_call(["cjpeg", "-version"])
+ return True
+ except subprocess.CalledProcessError: # pragma: no cover
+ return False
+ return False
-def netpbm_available():
+def netpbm_available() -> bool:
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
-def magick_command():
+def magick_command() -> list[str] | None:
if sys.platform == "win32":
magickhome = os.environ.get("MAGICK_HOME")
if magickhome:
@@ -286,47 +311,48 @@ def magick_command():
return imagemagick
if graphicsmagick and shutil.which(graphicsmagick[0]):
return graphicsmagick
+ return None
-def on_appveyor():
+def on_appveyor() -> bool:
return "APPVEYOR" in os.environ
-def on_github_actions():
+def on_github_actions() -> bool:
return "GITHUB_ACTIONS" in os.environ
-def on_ci():
+def on_ci() -> bool:
# GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ
-def is_big_endian():
+def is_big_endian() -> bool:
return sys.byteorder == "big"
-def is_ppc64le():
+def is_ppc64le() -> bool:
import platform
return platform.machine() == "ppc64le"
-def is_win32():
+def is_win32() -> bool:
return sys.platform.startswith("win32")
-def is_pypy():
+def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info")
-def is_mingw():
+def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw"
class CachedProperty:
- def __init__(self, func):
+ def __init__(self, func: Callable[[Any], None]) -> None:
self.func = func
- def __get__(self, instance, cls=None):
+ def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
result = instance.__dict__[self.func.__name__] = self.func(instance)
return result
diff --git a/Tests/images/apng/different_durations.png b/Tests/images/apng/different_durations.png
new file mode 100644
index 000000000..984254b8e
Binary files /dev/null and b/Tests/images/apng/different_durations.png differ
diff --git a/Tests/images/apng/mode_greyscale.png b/Tests/images/apng/mode_grayscale.png
similarity index 100%
rename from Tests/images/apng/mode_greyscale.png
rename to Tests/images/apng/mode_grayscale.png
diff --git a/Tests/images/apng/mode_greyscale_alpha.png b/Tests/images/apng/mode_grayscale_alpha.png
similarity index 100%
rename from Tests/images/apng/mode_greyscale_alpha.png
rename to Tests/images/apng/mode_grayscale_alpha.png
diff --git a/Tests/images/background_outside_palette.gif b/Tests/images/background_outside_palette.gif
new file mode 100644
index 000000000..63e767463
Binary files /dev/null and b/Tests/images/background_outside_palette.gif differ
diff --git a/Tests/images/bc1.dds b/Tests/images/bc1.dds
new file mode 100755
index 000000000..faec63a00
Binary files /dev/null and b/Tests/images/bc1.dds differ
diff --git a/Tests/images/bc1_typeless.dds b/Tests/images/bc1_typeless.dds
new file mode 100755
index 000000000..47a85e2d0
Binary files /dev/null and b/Tests/images/bc1_typeless.dds differ
diff --git a/Tests/images/bc4_typeless.dds b/Tests/images/bc4_typeless.dds
new file mode 100644
index 000000000..27f87889f
Binary files /dev/null and b/Tests/images/bc4_typeless.dds differ
diff --git a/Tests/images/bc4_unorm.dds b/Tests/images/bc4_unorm.dds
new file mode 100644
index 000000000..13da711bd
Binary files /dev/null and b/Tests/images/bc4_unorm.dds differ
diff --git a/Tests/images/bc4_unorm.png b/Tests/images/bc4_unorm.png
new file mode 100644
index 000000000..71d536c84
Binary files /dev/null and b/Tests/images/bc4_unorm.png differ
diff --git a/Tests/images/bc4u.dds b/Tests/images/bc4u.dds
new file mode 100644
index 000000000..7f9f050b6
Binary files /dev/null and b/Tests/images/bc4u.dds differ
diff --git a/Tests/images/bgr15.dds b/Tests/images/bgr15.dds
new file mode 100644
index 000000000..ba3bbddca
Binary files /dev/null and b/Tests/images/bgr15.dds differ
diff --git a/Tests/images/bgr15.png b/Tests/images/bgr15.png
new file mode 100644
index 000000000..a15ab5ad2
Binary files /dev/null and b/Tests/images/bgr15.png differ
diff --git a/Tests/images/bitmap_font_blend.png b/Tests/images/bitmap_font_blend.png
new file mode 100644
index 000000000..a5acf3667
Binary files /dev/null and b/Tests/images/bitmap_font_blend.png differ
diff --git a/Tests/images/bitmap_font_stroke_basic.png b/Tests/images/bitmap_font_stroke_basic.png
index 86b2d09f6..26aa3ab8e 100644
Binary files a/Tests/images/bitmap_font_stroke_basic.png and b/Tests/images/bitmap_font_stroke_basic.png differ
diff --git a/Tests/images/bitmap_font_stroke_raqm.png b/Tests/images/bitmap_font_stroke_raqm.png
index 08029ce34..be273d7cb 100644
Binary files a/Tests/images/bitmap_font_stroke_raqm.png and b/Tests/images/bitmap_font_stroke_raqm.png differ
diff --git a/Tests/images/cbdt.png b/Tests/images/cbdt.png
new file mode 100644
index 000000000..542bb812e
Binary files /dev/null and b/Tests/images/cbdt.png differ
diff --git a/Tests/images/cbdt_mask.png b/Tests/images/cbdt_mask.png
new file mode 100644
index 000000000..b0854a605
Binary files /dev/null and b/Tests/images/cbdt_mask.png differ
diff --git a/Tests/images/cbdt_notocoloremoji.png b/Tests/images/cbdt_notocoloremoji.png
deleted file mode 100644
index 1da12fba1..000000000
Binary files a/Tests/images/cbdt_notocoloremoji.png and /dev/null differ
diff --git a/Tests/images/cbdt_notocoloremoji_mask.png b/Tests/images/cbdt_notocoloremoji_mask.png
deleted file mode 100644
index 6d036a0b6..000000000
Binary files a/Tests/images/cbdt_notocoloremoji_mask.png and /dev/null differ
diff --git a/Tests/images/create_eps.gnuplot b/Tests/images/create_eps.gnuplot
index 4d7e29877..57a3c8c97 100644
--- a/Tests/images/create_eps.gnuplot
+++ b/Tests/images/create_eps.gnuplot
@@ -1,5 +1,3 @@
-#!/usr/bin/gnuplot
-
#This is the script that was used to create our sample EPS files
#We used the following version of the gnuplot program
#G N U P L O T
diff --git a/Tests/images/default_font_freetype.png b/Tests/images/default_font_freetype.png
index e00bb5d85..bc1654a25 100644
Binary files a/Tests/images/default_font_freetype.png and b/Tests/images/default_font_freetype.png differ
diff --git a/Tests/images/five_channels.psd b/Tests/images/five_channels.psd
new file mode 100644
index 000000000..021a5fa63
Binary files /dev/null and b/Tests/images/five_channels.psd differ
diff --git a/Tests/images/hopper.pfm b/Tests/images/hopper.pfm
new file mode 100644
index 000000000..b57661564
Binary files /dev/null and b/Tests/images/hopper.pfm differ
diff --git a/Tests/images/hopper_be.pfm b/Tests/images/hopper_be.pfm
new file mode 100644
index 000000000..93c75e26f
Binary files /dev/null and b/Tests/images/hopper_be.pfm differ
diff --git a/Tests/images/hopper_rle8_greyscale.bmp b/Tests/images/hopper_rle8_grayscale.bmp
similarity index 100%
rename from Tests/images/hopper_rle8_greyscale.bmp
rename to Tests/images/hopper_rle8_grayscale.bmp
diff --git a/Tests/images/multiple_exif.jpg b/Tests/images/multiple_exif.jpg
new file mode 100644
index 000000000..32e0aa301
Binary files /dev/null and b/Tests/images/multiple_exif.jpg differ
diff --git a/Tests/images/test_combine_caron_below_ttb.png b/Tests/images/test_combine_caron_below_ttb.png
index 5c7576de0..2b7cc89ea 100644
Binary files a/Tests/images/test_combine_caron_below_ttb.png and b/Tests/images/test_combine_caron_below_ttb.png differ
diff --git a/Tests/images/test_combine_caron_below_ttb_lb.png b/Tests/images/test_combine_caron_below_ttb_lb.png
index bacd6a141..3ced2dbfc 100644
Binary files a/Tests/images/test_combine_caron_below_ttb_lb.png and b/Tests/images/test_combine_caron_below_ttb_lb.png differ
diff --git a/Tests/images/test_combine_caron_ttb.png b/Tests/images/test_combine_caron_ttb.png
index a94be2f0a..569cc1ec3 100644
Binary files a/Tests/images/test_combine_caron_ttb.png and b/Tests/images/test_combine_caron_ttb.png differ
diff --git a/Tests/images/test_combine_caron_ttb_lt.png b/Tests/images/test_combine_caron_ttb_lt.png
index a94be2f0a..569cc1ec3 100644
Binary files a/Tests/images/test_combine_caron_ttb_lt.png and b/Tests/images/test_combine_caron_ttb_lt.png differ
diff --git a/Tests/images/unimplemented_pixel_format.dds b/Tests/images/unimplemented_pfflags.dds
similarity index 99%
rename from Tests/images/unimplemented_pixel_format.dds
rename to Tests/images/unimplemented_pfflags.dds
index 41a343886..e3fc8344d 100755
Binary files a/Tests/images/unimplemented_pixel_format.dds and b/Tests/images/unimplemented_pfflags.dds differ
diff --git a/Tests/images/unsupported_bitcount.dds b/Tests/images/unsupported_bitcount.dds
new file mode 100644
index 000000000..f9bb82254
Binary files /dev/null and b/Tests/images/unsupported_bitcount.dds differ
diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh
index 37fad7bc8..3aa6c7f6a 100755
--- a/Tests/oss-fuzz/build.sh
+++ b/Tests/oss-fuzz/build.sh
@@ -15,7 +15,7 @@
#
################################################################################
-python3 setup.py build --build-base=/tmp/build install
+python3 -m pip install .
# Build fuzzers in $OUT.
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py
index bc2ba9a7e..8788d7021 100755
--- a/Tests/oss-fuzz/fuzz_font.py
+++ b/Tests/oss-fuzz/fuzz_font.py
@@ -23,7 +23,7 @@ with atheris.instrument_imports():
import fuzzers
-def TestOneInput(data):
+def TestOneInput(data: bytes) -> None:
try:
fuzzers.fuzz_font(data)
except Exception:
@@ -32,7 +32,7 @@ def TestOneInput(data):
pass
-def main():
+def main() -> None:
fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py
index 545daccb6..9137391b6 100644
--- a/Tests/oss-fuzz/fuzz_pillow.py
+++ b/Tests/oss-fuzz/fuzz_pillow.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python3
-
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -23,7 +21,7 @@ with atheris.instrument_imports():
import fuzzers
-def TestOneInput(data):
+def TestOneInput(data: bytes) -> None:
try:
fuzzers.fuzz_image(data)
except Exception:
@@ -32,7 +30,7 @@ def TestOneInput(data):
pass
-def main():
+def main() -> None:
fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py
index 10a172b46..d6c1fab71 100644
--- a/Tests/oss-fuzz/fuzzers.py
+++ b/Tests/oss-fuzz/fuzzers.py
@@ -1,21 +1,23 @@
+from __future__ import annotations
+
import io
import warnings
from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont
-def enable_decompressionbomb_error():
+def enable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
warnings.filterwarnings("ignore")
warnings.simplefilter("error", Image.DecompressionBombWarning)
-def disable_decompressionbomb_error():
+def disable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False
warnings.resetwarnings()
-def fuzz_image(data):
+def fuzz_image(data: bytes) -> None:
# This will fail on some images in the corpus, as we have many
# invalid images in the test suite.
with Image.open(io.BytesIO(data)) as im:
@@ -24,7 +26,7 @@ def fuzz_image(data):
im.save(io.BytesIO(), "BMP")
-def fuzz_font(data):
+def fuzz_font(data: bytes) -> None:
wrapper = io.BytesIO(data)
try:
font = ImageFont.truetype(wrapper)
diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py
index 0526f550e..459cc1a37 100644
--- a/Tests/oss-fuzz/test_fuzzers.py
+++ b/Tests/oss-fuzz/test_fuzzers.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import subprocess
import sys
@@ -22,7 +24,7 @@ if features.check("libjpeg_turbo"):
"path",
subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"),
)
-def test_fuzz_images(path):
+def test_fuzz_images(path: str) -> None:
fuzzers.enable_decompressionbomb_error()
try:
with open(path, "rb") as f:
@@ -53,7 +55,7 @@ def test_fuzz_images(path):
@pytest.mark.parametrize(
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
)
-def test_fuzz_fonts(path):
+def test_fuzz_fonts(path: str) -> None:
if not path:
return
with open(path, "rb") as f:
diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py
index 3fd982474..c3926250f 100644
--- a/Tests/test_000_sanity.py
+++ b/Tests/test_000_sanity.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from PIL import Image
-def test_sanity():
+def test_sanity() -> None:
# Make sure we have the binary extension
Image.core.new("L", (100, 100))
diff --git a/Tests/test_binary.py b/Tests/test_binary.py
index 4882e65e6..d19799a09 100644
--- a/Tests/test_binary.py
+++ b/Tests/test_binary.py
@@ -1,12 +1,14 @@
+from __future__ import annotations
+
from PIL import _binary
-def test_standard():
+def test_standard() -> None:
assert _binary.i8(b"*") == 42
assert _binary.o8(42) == b"*"
-def test_little_endian():
+def test_little_endian() -> None:
assert _binary.i16le(b"\xff\xff\x00\x00") == 65535
assert _binary.i32le(b"\xff\xff\x00\x00") == 65535
@@ -14,7 +16,7 @@ def test_little_endian():
assert _binary.o32le(65535) == b"\xff\xff\x00\x00"
-def test_big_endian():
+def test_big_endian() -> None:
assert _binary.i16be(b"\x00\x00\xff\xff") == 0
assert _binary.i32be(b"\x00\x00\xff\xff") == 65535
diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py
index 002a44a4f..22ac9443e 100644
--- a/Tests/test_bmp_reference.py
+++ b/Tests/test_bmp_reference.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import os
import warnings
@@ -8,13 +10,13 @@ from .helper import assert_image_similar
base = os.path.join("Tests", "images", "bmp")
-def get_files(d, ext=".bmp"):
+def get_files(d, ext: str = ".bmp"):
return [
os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f
]
-def test_bad():
+def test_bad() -> None:
"""These shouldn't crash/dos, but they shouldn't return anything
either"""
for f in get_files("b"):
@@ -54,7 +56,7 @@ def test_questionable():
raise
-def test_good():
+def test_good() -> None:
"""These should all work. There's a set of target files in the
html directory that we can compare against."""
diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py
index 745364ddc..dfedb48d9 100644
--- a/Tests/test_box_blur.py
+++ b/Tests/test_box_blur.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageFilter
@@ -14,18 +16,18 @@ sample.putdata(sum([
# fmt: on
-def test_imageops_box_blur():
+def test_imageops_box_blur() -> None:
i = sample.filter(ImageFilter.BoxBlur(1))
assert i.mode == sample.mode
assert i.size == sample.size
assert isinstance(i, Image.Image)
-def box_blur(image, radius=1, n=1):
+def box_blur(image, radius: int = 1, n: int = 1):
return image._new(image.im.box_blur((radius, radius), n))
-def assert_image(im, data, delta=0):
+def assert_image(im, data, delta: int = 0) -> None:
it = iter(im.getdata())
for data_row in data:
im_row = [next(it) for _ in range(im.size[0])]
@@ -35,7 +37,7 @@ def assert_image(im, data, delta=0):
next(it)
-def assert_blur(im, radius, data, passes=1, delta=0):
+def assert_blur(im, radius, data, passes: int = 1, delta: int = 0) -> None:
# check grayscale image
assert_image(box_blur(im, radius, passes), data, delta)
rgba = Image.merge("RGBA", (im, im, im, im))
@@ -43,7 +45,7 @@ def assert_blur(im, radius, data, passes=1, delta=0):
assert_image(band, data, delta)
-def test_color_modes():
+def test_color_modes() -> None:
with pytest.raises(ValueError):
box_blur(sample.convert("1"))
with pytest.raises(ValueError):
@@ -63,7 +65,7 @@ def test_color_modes():
box_blur(sample.convert("YCbCr"))
-def test_radius_0():
+def test_radius_0() -> None:
assert_blur(
sample,
0,
@@ -79,7 +81,7 @@ def test_radius_0():
)
-def test_radius_0_02():
+def test_radius_0_02() -> None:
assert_blur(
sample,
0.02,
@@ -96,7 +98,7 @@ def test_radius_0_02():
)
-def test_radius_0_05():
+def test_radius_0_05() -> None:
assert_blur(
sample,
0.05,
@@ -113,7 +115,7 @@ def test_radius_0_05():
)
-def test_radius_0_1():
+def test_radius_0_1() -> None:
assert_blur(
sample,
0.1,
@@ -130,7 +132,7 @@ def test_radius_0_1():
)
-def test_radius_0_5():
+def test_radius_0_5() -> None:
assert_blur(
sample,
0.5,
@@ -147,7 +149,7 @@ def test_radius_0_5():
)
-def test_radius_1():
+def test_radius_1() -> None:
assert_blur(
sample,
1,
@@ -164,7 +166,7 @@ def test_radius_1():
)
-def test_radius_1_5():
+def test_radius_1_5() -> None:
assert_blur(
sample,
1.5,
@@ -181,7 +183,7 @@ def test_radius_1_5():
)
-def test_radius_bigger_then_half():
+def test_radius_bigger_then_half() -> None:
assert_blur(
sample,
3,
@@ -198,7 +200,7 @@ def test_radius_bigger_then_half():
)
-def test_radius_bigger_then_width():
+def test_radius_bigger_then_width() -> None:
assert_blur(
sample,
10,
@@ -213,7 +215,7 @@ def test_radius_bigger_then_width():
)
-def test_extreme_large_radius():
+def test_extreme_large_radius() -> None:
assert_blur(
sample,
600,
@@ -228,7 +230,7 @@ def test_extreme_large_radius():
)
-def test_two_passes():
+def test_two_passes() -> None:
assert_blur(
sample,
1,
@@ -246,7 +248,7 @@ def test_two_passes():
)
-def test_three_passes():
+def test_three_passes() -> None:
assert_blur(
sample,
1,
diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py
index 6d9a60570..e6c8d7819 100644
--- a/Tests/test_color_lut.py
+++ b/Tests/test_color_lut.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from array import array
import pytest
@@ -39,7 +41,7 @@ class TestColorLut3DCoreAPI:
[item for sublist in table for item in sublist],
)
- def test_wrong_args(self):
+ def test_wrong_args(self) -> None:
im = Image.new("RGB", (10, 10), 0)
with pytest.raises(ValueError, match="filter"):
@@ -99,7 +101,7 @@ class TestColorLut3DCoreAPI:
with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
- def test_correct_args(self):
+ def test_correct_args(self) -> None:
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
@@ -134,7 +136,7 @@ class TestColorLut3DCoreAPI:
*self.generate_identity_table(3, (3, 3, 65)),
)
- def test_wrong_mode(self):
+ def test_wrong_mode(self) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
@@ -165,7 +167,7 @@ class TestColorLut3DCoreAPI:
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
- def test_correct_mode(self):
+ def test_correct_mode(self) -> None:
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
@@ -186,7 +188,7 @@ class TestColorLut3DCoreAPI:
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
- def test_identities(self):
+ def test_identities(self) -> None:
g = Image.linear_gradient("L")
im = Image.merge(
"RGB",
@@ -222,7 +224,7 @@ class TestColorLut3DCoreAPI:
),
)
- def test_identities_4_channels(self):
+ def test_identities_4_channels(self) -> None:
g = Image.linear_gradient("L")
im = Image.merge(
"RGB",
@@ -245,7 +247,7 @@ class TestColorLut3DCoreAPI:
),
)
- def test_copy_alpha_channel(self):
+ def test_copy_alpha_channel(self) -> None:
g = Image.linear_gradient("L")
im = Image.merge(
"RGBA",
@@ -268,7 +270,7 @@ class TestColorLut3DCoreAPI:
),
)
- def test_channels_order(self):
+ def test_channels_order(self) -> None:
g = Image.linear_gradient("L")
im = Image.merge(
"RGB",
@@ -293,7 +295,7 @@ class TestColorLut3DCoreAPI:
])))
# fmt: on
- def test_overflow(self):
+ def test_overflow(self) -> None:
g = Image.linear_gradient("L")
im = Image.merge(
"RGB",
@@ -346,7 +348,7 @@ class TestColorLut3DCoreAPI:
class TestColorLut3DFilter:
- def test_wrong_args(self):
+ def test_wrong_args(self) -> None:
with pytest.raises(ValueError, match="should be either an integer"):
ImageFilter.Color3DLUT("small", [1])
@@ -374,7 +376,7 @@ class TestColorLut3DFilter:
with pytest.raises(ValueError, match="Only 3 or 4 output"):
ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2)
- def test_convert_table(self):
+ def test_convert_table(self) -> None:
lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
assert tuple(lut.size) == (2, 2, 2)
assert lut.name == "Color 3D LUT"
@@ -392,7 +394,7 @@ class TestColorLut3DFilter:
assert lut.table == list(range(4)) * 8
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
- def test_numpy_sources(self):
+ def test_numpy_sources(self) -> None:
table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16)
with pytest.raises(ValueError, match="should have either channels"):
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
@@ -425,7 +427,7 @@ class TestColorLut3DFilter:
assert lut.table[0] == 33
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
- def test_numpy_formats(self):
+ def test_numpy_formats(self) -> None:
g = Image.linear_gradient("L")
im = Image.merge(
"RGB",
@@ -464,7 +466,7 @@ class TestColorLut3DFilter:
lut.table = numpy.array(lut.table, dtype=numpy.int8)
im.filter(lut)
- def test_repr(self):
+ def test_repr(self) -> None:
lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
assert repr(lut) == ""
@@ -482,7 +484,7 @@ class TestColorLut3DFilter:
class TestGenerateColorLut3D:
- def test_wrong_channels_count(self):
+ def test_wrong_channels_count(self) -> None:
with pytest.raises(ValueError, match="3 or 4 output channels"):
ImageFilter.Color3DLUT.generate(
5, channels=2, callback=lambda r, g, b: (r, g, b)
@@ -496,7 +498,7 @@ class TestGenerateColorLut3D:
5, channels=4, callback=lambda r, g, b: (r, g, b)
)
- def test_3_channels(self):
+ def test_3_channels(self) -> None:
lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
assert tuple(lut.size) == (5, 5, 5)
assert lut.name == "Color 3D LUT"
@@ -506,7 +508,7 @@ class TestGenerateColorLut3D:
1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0]
# fmt: on
- def test_4_channels(self):
+ def test_4_channels(self) -> None:
lut = ImageFilter.Color3DLUT.generate(
5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2)
)
@@ -519,7 +521,7 @@ class TestGenerateColorLut3D:
]
# fmt: on
- def test_apply(self):
+ def test_apply(self) -> None:
lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
g = Image.linear_gradient("L")
@@ -535,7 +537,7 @@ class TestGenerateColorLut3D:
class TestTransformColorLut3D:
- def test_wrong_args(self):
+ def test_wrong_args(self) -> None:
source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
with pytest.raises(ValueError, match="Only 3 or 4 output"):
@@ -550,7 +552,7 @@ class TestTransformColorLut3D:
with pytest.raises(TypeError):
source.transform(lambda r, g, b, a: (r, g, b))
- def test_target_mode(self):
+ def test_target_mode(self) -> None:
source = ImageFilter.Color3DLUT.generate(
2, lambda r, g, b: (r, g, b), target_mode="HSV"
)
@@ -561,7 +563,7 @@ class TestTransformColorLut3D:
lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB")
assert lut.mode == "RGB"
- def test_3_to_3_channels(self):
+ def test_3_to_3_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b))
lut = source.transform(lambda r, g, b: (r * r, g * g, b * b))
assert tuple(lut.size) == tuple(source.size)
@@ -569,7 +571,7 @@ class TestTransformColorLut3D:
assert lut.table != source.table
assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
- def test_3_to_4_channels(self):
+ def test_3_to_4_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b))
lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4)
assert tuple(lut.size) == tuple(source.size)
@@ -581,7 +583,7 @@ class TestTransformColorLut3D:
0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]
# fmt: on
- def test_4_to_3_channels(self):
+ def test_4_to_3_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate(
(3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4
)
@@ -597,7 +599,7 @@ class TestTransformColorLut3D:
1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]
# fmt: on
- def test_4_to_4_channels(self):
+ def test_4_to_4_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate(
(6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4
)
@@ -611,7 +613,7 @@ class TestTransformColorLut3D:
0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]
# fmt: on
- def test_with_normals_3_channels(self):
+ def test_with_normals_3_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate(
(6, 5, 4), lambda r, g, b: (r * r, g * g, b * b)
)
@@ -627,7 +629,7 @@ class TestTransformColorLut3D:
0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]
# fmt: on
- def test_with_normals_4_channels(self):
+ def test_with_normals_4_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate(
(3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4
)
diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py
index 9021a9fb3..5eabe8f11 100644
--- a/Tests/test_core_resources.py
+++ b/Tests/test_core_resources.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import sys
import pytest
@@ -7,7 +9,7 @@ from PIL import Image
from .helper import is_pypy
-def test_get_stats():
+def test_get_stats() -> None:
# Create at least one image
Image.new("RGB", (10, 10))
@@ -20,7 +22,7 @@ def test_get_stats():
assert "blocks_cached" in stats
-def test_reset_stats():
+def test_reset_stats() -> None:
Image.core.reset_stats()
stats = Image.core.get_stats()
@@ -33,19 +35,19 @@ def test_reset_stats():
class TestCoreMemory:
- def teardown_method(self):
+ def teardown_method(self) -> None:
# Restore default values
Image.core.set_alignment(1)
Image.core.set_block_size(1024 * 1024)
Image.core.set_blocks_max(0)
Image.core.clear_cache()
- def test_get_alignment(self):
+ def test_get_alignment(self) -> None:
alignment = Image.core.get_alignment()
assert alignment > 0
- def test_set_alignment(self):
+ def test_set_alignment(self) -> None:
for i in [1, 2, 4, 8, 16, 32]:
Image.core.set_alignment(i)
alignment = Image.core.get_alignment()
@@ -61,12 +63,12 @@ class TestCoreMemory:
with pytest.raises(ValueError):
Image.core.set_alignment(3)
- def test_get_block_size(self):
+ def test_get_block_size(self) -> None:
block_size = Image.core.get_block_size()
assert block_size >= 4096
- def test_set_block_size(self):
+ def test_set_block_size(self) -> None:
for i in [4096, 2 * 4096, 3 * 4096]:
Image.core.set_block_size(i)
block_size = Image.core.get_block_size()
@@ -82,7 +84,7 @@ class TestCoreMemory:
with pytest.raises(ValueError):
Image.core.set_block_size(4000)
- def test_set_block_size_stats(self):
+ def test_set_block_size_stats(self) -> None:
Image.core.reset_stats()
Image.core.set_blocks_max(0)
Image.core.set_block_size(4096)
@@ -94,12 +96,12 @@ class TestCoreMemory:
if not is_pypy():
assert stats["freed_blocks"] >= 64
- def test_get_blocks_max(self):
+ def test_get_blocks_max(self) -> None:
blocks_max = Image.core.get_blocks_max()
assert blocks_max >= 0
- def test_set_blocks_max(self):
+ def test_set_blocks_max(self) -> None:
for i in [0, 1, 10]:
Image.core.set_blocks_max(i)
blocks_max = Image.core.get_blocks_max()
@@ -115,7 +117,7 @@ class TestCoreMemory:
Image.core.set_blocks_max(2**29)
@pytest.mark.skipif(is_pypy(), reason="Images not collected")
- def test_set_blocks_max_stats(self):
+ def test_set_blocks_max_stats(self) -> None:
Image.core.reset_stats()
Image.core.set_blocks_max(128)
Image.core.set_block_size(4096)
@@ -130,7 +132,7 @@ class TestCoreMemory:
assert stats["blocks_cached"] == 64
@pytest.mark.skipif(is_pypy(), reason="Images not collected")
- def test_clear_cache_stats(self):
+ def test_clear_cache_stats(self) -> None:
Image.core.reset_stats()
Image.core.clear_cache()
Image.core.set_blocks_max(128)
@@ -147,7 +149,7 @@ class TestCoreMemory:
assert stats["freed_blocks"] >= 48
assert stats["blocks_cached"] == 16
- def test_large_images(self):
+ def test_large_images(self) -> None:
Image.core.reset_stats()
Image.core.set_blocks_max(0)
Image.core.set_block_size(4096)
@@ -164,14 +166,14 @@ class TestCoreMemory:
class TestEnvVars:
- def teardown_method(self):
+ def teardown_method(self) -> None:
# Restore default values
Image.core.set_alignment(1)
Image.core.set_block_size(1024 * 1024)
Image.core.set_blocks_max(0)
Image.core.clear_cache()
- def test_units(self):
+ def test_units(self) -> None:
Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"})
assert Image.core.get_blocks_max() == 2 * 1024
Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"})
@@ -185,6 +187,6 @@ class TestEnvVars:
{"PILLOW_BLOCKS_MAX": "wat"},
),
)
- def test_warnings(self, var):
+ def test_warnings(self, var) -> None:
with pytest.warns(UserWarning):
Image._apply_env_variables(var)
diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py
index 87681a0b5..9c21efa45 100644
--- a/Tests/test_decompression_bomb.py
+++ b/Tests/test_decompression_bomb.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -10,16 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb:
- def teardown_method(self, method):
+ def teardown_method(self, method) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
- def test_no_warning_small_file(self):
+ def test_no_warning_small_file(self) -> None:
# Implicit assert: no warning.
# A warning would cause a failure.
with Image.open(TEST_FILE):
pass
- def test_no_warning_no_limit(self):
+ def test_no_warning_no_limit(self) -> None:
# Arrange
# Turn limit off
Image.MAX_IMAGE_PIXELS = None
@@ -31,7 +33,7 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE):
pass
- def test_warning(self):
+ def test_warning(self) -> None:
# Set limit to trigger warning on the test file
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
@@ -40,7 +42,7 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE):
pass
- def test_exception(self):
+ def test_exception(self) -> None:
# Set limit to trigger exception on the test file
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1
@@ -49,22 +51,22 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE):
pass
- def test_exception_ico(self):
+ def test_exception_ico(self) -> None:
with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/decompression_bomb.ico"):
pass
- def test_exception_gif(self):
+ def test_exception_gif(self) -> None:
with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/decompression_bomb.gif"):
pass
- def test_exception_gif_extents(self):
+ def test_exception_gif_extents(self) -> None:
with Image.open("Tests/images/decompression_bomb_extents.gif") as im:
with pytest.raises(Image.DecompressionBombError):
im.seek(1)
- def test_exception_gif_zero_width(self):
+ def test_exception_gif_zero_width(self) -> None:
# Set limit to trigger exception on the test file
Image.MAX_IMAGE_PIXELS = 4 * 64 * 128
assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128
@@ -73,7 +75,7 @@ class TestDecompressionBomb:
with Image.open("Tests/images/zero_width.gif"):
pass
- def test_exception_bmp(self):
+ def test_exception_bmp(self) -> None:
with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/bmp/b/reallybig.bmp"):
pass
@@ -81,15 +83,15 @@ class TestDecompressionBomb:
class TestDecompressionCrop:
@classmethod
- def setup_class(cls):
+ def setup_class(cls) -> None:
width, height = 128, 128
Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
@classmethod
- def teardown_class(cls):
+ def teardown_class(cls) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
- def test_enlarge_crop(self):
+ def test_enlarge_crop(self) -> None:
# Crops can extend the extents, therefore we should have the
# same decompression bomb warnings on them.
with hopper() as src:
@@ -97,7 +99,7 @@ class TestDecompressionCrop:
with pytest.warns(Image.DecompressionBombWarning):
src.crop(box)
- def test_crop_decompression_checks(self):
+ def test_crop_decompression_checks(self) -> None:
im = Image.new("RGB", (100, 100))
for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)):
diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py
index f175b90af..6ffc8f6f5 100644
--- a/Tests/test_deprecate.py
+++ b/Tests/test_deprecate.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import _deprecate
@@ -18,12 +20,12 @@ from PIL import _deprecate
),
],
)
-def test_version(version, expected):
+def test_version(version, expected) -> None:
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", version, "new thing")
-def test_unknown_version():
+def test_unknown_version() -> None:
expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?"
with pytest.raises(ValueError, match=expected):
_deprecate.deprecate("Old thing", 12345, "new thing")
@@ -44,13 +46,13 @@ def test_unknown_version():
),
],
)
-def test_old_version(deprecated, plural, expected):
+def test_old_version(deprecated, plural, expected) -> None:
expected = r""
with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural)
-def test_plural():
+def test_plural() -> None:
expected = (
r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Use new thing instead\."
@@ -59,7 +61,7 @@ def test_plural():
_deprecate.deprecate("Old things", 11, "new thing", plural=True)
-def test_replacement_and_action():
+def test_replacement_and_action() -> None:
expected = "Use only one of 'replacement' and 'action'"
with pytest.raises(ValueError, match=expected):
_deprecate.deprecate(
@@ -74,7 +76,7 @@ def test_replacement_and_action():
"Upgrade to new thing.",
],
)
-def test_action(action):
+def test_action(action) -> None:
expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Upgrade to new thing\."
@@ -83,7 +85,7 @@ def test_action(action):
_deprecate.deprecate("Old thing", 11, action=action)
-def test_no_replacement_or_action():
+def test_no_replacement_or_action() -> None:
expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)"
)
diff --git a/Tests/test_features.py b/Tests/test_features.py
index c4e9cd368..de74e9c18 100644
--- a/Tests/test_features.py
+++ b/Tests/test_features.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import io
import re
@@ -13,7 +15,7 @@ except ImportError:
pass
-def test_check():
+def test_check() -> None:
# Check the correctness of the convenience function
for module in features.modules:
assert features.check_module(module) == features.check(module)
@@ -23,11 +25,11 @@ def test_check():
assert features.check_feature(feature) == features.check(feature)
-def test_version():
+def test_version() -> None:
# Check the correctness of the convenience function
# and the format of version numbers
- def test(name, function):
+ def test(name, function) -> None:
version = features.version(name)
if not features.check(name):
assert version is None
@@ -45,56 +47,56 @@ def test_version():
@skip_unless_feature("webp")
-def test_webp_transparency():
+def test_webp_transparency() -> None:
assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha()
assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY
@skip_unless_feature("webp")
-def test_webp_mux():
+def test_webp_mux() -> None:
assert features.check("webp_mux") == _webp.HAVE_WEBPMUX
@skip_unless_feature("webp")
-def test_webp_anim():
+def test_webp_anim() -> None:
assert features.check("webp_anim") == _webp.HAVE_WEBPANIM
@skip_unless_feature("libjpeg_turbo")
-def test_libjpeg_turbo_version():
+def test_libjpeg_turbo_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo"))
@skip_unless_feature("libimagequant")
-def test_libimagequant_version():
+def test_libimagequant_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
@pytest.mark.parametrize("feature", features.modules)
-def test_check_modules(feature):
+def test_check_modules(feature) -> None:
assert features.check_module(feature) in [True, False]
@pytest.mark.parametrize("feature", features.codecs)
-def test_check_codecs(feature):
+def test_check_codecs(feature) -> None:
assert features.check_codec(feature) in [True, False]
-def test_check_warns_on_nonexistent():
+def test_check_warns_on_nonexistent() -> None:
with pytest.warns(UserWarning) as cm:
has_feature = features.check("typo")
assert has_feature is False
assert str(cm[-1].message) == "Unknown feature 'typo'."
-def test_supported_modules():
+def test_supported_modules() -> None:
assert isinstance(features.get_supported_modules(), list)
assert isinstance(features.get_supported_codecs(), list)
assert isinstance(features.get_supported_features(), list)
assert isinstance(features.get_supported(), list)
-def test_unsupported_codec():
+def test_unsupported_codec() -> None:
# Arrange
codec = "unsupported_codec"
# Act / Assert
@@ -104,7 +106,7 @@ def test_unsupported_codec():
features.version_codec(codec)
-def test_unsupported_module():
+def test_unsupported_module() -> None:
# Arrange
module = "unsupported_module"
# Act / Assert
@@ -114,7 +116,7 @@ def test_unsupported_module():
features.version_module(module)
-def test_pilinfo():
+def test_pilinfo() -> None:
buf = io.StringIO()
features.pilinfo(buf)
out = buf.getvalue()
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index 579288808..f9edf6e98 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImageSequence, PngImagePlugin
@@ -6,7 +10,7 @@ from PIL import Image, ImageSequence, PngImagePlugin
# APNG browser support tests and fixtures via:
# https://philip.html5.org/tests/apng/tests.html
# (referenced from https://wiki.mozilla.org/APNG_Specification)
-def test_apng_basic():
+def test_apng_basic() -> None:
with Image.open("Tests/images/apng/single_frame.png") as im:
assert not im.is_animated
assert im.n_frames == 1
@@ -43,14 +47,14 @@ def test_apng_basic():
"filename",
("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
)
-def test_apng_fdat(filename):
+def test_apng_fdat(filename) -> None:
with Image.open(filename) as im:
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-def test_apng_dispose():
+def test_apng_dispose() -> None:
with Image.open("Tests/images/apng/dispose_op_none.png") as im:
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
@@ -82,7 +86,7 @@ def test_apng_dispose():
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
-def test_apng_dispose_region():
+def test_apng_dispose_region() -> None:
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
@@ -104,7 +108,7 @@ def test_apng_dispose_region():
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-def test_apng_dispose_op_previous_frame():
+def test_apng_dispose_op_previous_frame() -> None:
# Test that the dispose settings being used are from the previous frame
#
# Image created with:
@@ -129,14 +133,14 @@ def test_apng_dispose_op_previous_frame():
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
-def test_apng_dispose_op_background_p_mode():
+def test_apng_dispose_op_background_p_mode() -> None:
with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im:
im.seek(1)
im.load()
assert im.size == (128, 64)
-def test_apng_blend():
+def test_apng_blend() -> None:
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
@@ -163,20 +167,20 @@ def test_apng_blend():
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-def test_apng_blend_transparency():
+def test_apng_blend_transparency() -> None:
with Image.open("Tests/images/blend_transparency.png") as im:
im.seek(1)
assert im.getpixel((0, 0)) == (255, 0, 0)
-def test_apng_chunk_order():
+def test_apng_chunk_order() -> None:
with Image.open("Tests/images/apng/fctl_actl.png") as im:
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-def test_apng_delay():
+def test_apng_delay() -> None:
with Image.open("Tests/images/apng/delay.png") as im:
im.seek(1)
assert im.info.get("duration") == 500.0
@@ -216,7 +220,7 @@ def test_apng_delay():
assert im.info.get("duration") == 1000.0
-def test_apng_num_plays():
+def test_apng_num_plays() -> None:
with Image.open("Tests/images/apng/num_plays.png") as im:
assert im.info.get("loop") == 0
@@ -224,20 +228,20 @@ def test_apng_num_plays():
assert im.info.get("loop") == 1
-def test_apng_mode():
+def test_apng_mode() -> None:
with Image.open("Tests/images/apng/mode_16bit.png") as im:
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_greyscale.png") as im:
+ with Image.open("Tests/images/apng/mode_grayscale.png") as im:
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_greyscale_alpha.png") as im:
+ with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
assert im.mode == "LA"
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (128, 191)
@@ -265,7 +269,7 @@ def test_apng_mode():
assert im.getpixel((64, 32)) == (0, 0, 255, 128)
-def test_apng_chunk_errors():
+def test_apng_chunk_errors() -> None:
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
assert not im.is_animated
@@ -290,7 +294,7 @@ def test_apng_chunk_errors():
im.seek(im.n_frames - 1)
-def test_apng_syntax_errors():
+def test_apng_syntax_errors() -> None:
with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
assert not im.is_animated
@@ -334,14 +338,14 @@ def test_apng_syntax_errors():
"sequence_fdat_fctl.png",
),
)
-def test_apng_sequence_errors(test_file):
+def test_apng_sequence_errors(test_file) -> None:
with pytest.raises(SyntaxError):
with Image.open(f"Tests/images/apng/{test_file}") as im:
im.seek(im.n_frames - 1)
im.load()
-def test_apng_save(tmp_path):
+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")
im.save(test_file, save_all=True)
@@ -350,15 +354,13 @@ def test_apng_save(tmp_path):
im.load()
assert not im.is_animated
assert im.n_frames == 1
- assert im.get_format_mimetype() == "image/apng"
+ assert im.get_format_mimetype() == "image/png"
assert im.info.get("default_image") is None
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/single_frame_default.png") as im:
- frames = []
- for frame_im in ImageSequence.Iterator(im):
- frames.append(frame_im.copy())
+ frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)]
frames[0].save(
test_file, save_all=True, default_image=True, append_images=frames[1:]
)
@@ -374,7 +376,7 @@ def test_apng_save(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-def test_apng_save_alpha(tmp_path):
+def test_apng_save_alpha(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
@@ -388,7 +390,7 @@ def test_apng_save_alpha(tmp_path):
assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)
-def test_apng_save_split_fdat(tmp_path):
+def test_apng_save_split_fdat(tmp_path: Path) -> None:
# test to make sure we do not generate sequence errors when writing
# frames with image data spanning multiple fdAT chunks (in this case
# both the default image and first animation frame will span multiple
@@ -412,7 +414,7 @@ def test_apng_save_split_fdat(tmp_path):
assert exception is None
-def test_apng_save_duration_loop(tmp_path):
+def test_apng_save_duration_loop(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/apng/delay.png") as im:
frames = []
@@ -450,29 +452,32 @@ def test_apng_save_duration_loop(tmp_path):
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
)
with Image.open(test_file) as im:
- im.load()
assert im.n_frames == 1
- assert im.info.get("duration") == 750
+ assert "duration" not in im.info
+
+ different_frame = Image.new("RGBA", (128, 64))
+ frame.save(
+ test_file,
+ save_all=True,
+ append_images=[frame, different_frame],
+ duration=[500, 100, 150],
+ )
+ with Image.open(test_file) as im:
+ assert im.n_frames == 2
+ assert im.info["duration"] == 600
+
+ im.seek(1)
+ assert im.info["duration"] == 150
# test info duration
- frame.info["duration"] = 750
- frame.save(test_file, save_all=True)
+ frame.info["duration"] = 300
+ frame.save(test_file, save_all=True, append_images=[frame, different_frame])
with Image.open(test_file) as im:
- assert im.info.get("duration") == 750
+ assert im.n_frames == 2
+ assert im.info["duration"] == 600
-def test_apng_save_duplicate_duration(tmp_path):
- test_file = str(tmp_path / "temp.png")
- frame = Image.new("RGB", (1, 1))
-
- # Test a single duration is correctly combined across duplicate frames
- frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
- with Image.open(test_file) as im:
- assert im.n_frames == 1
- assert im.info.get("duration") == 1500
-
-
-def test_apng_save_disposal(tmp_path):
+def test_apng_save_disposal(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255))
@@ -573,7 +578,7 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
-def test_apng_save_disposal_previous(tmp_path):
+def test_apng_save_disposal_previous(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
size = (128, 64)
blue = Image.new("RGBA", size, (0, 0, 255, 255))
@@ -595,7 +600,7 @@ def test_apng_save_disposal_previous(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-def test_apng_save_blend(tmp_path):
+def test_apng_save_blend(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255))
@@ -663,7 +668,7 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
-def test_seek_after_close():
+def test_seek_after_close() -> None:
im = Image.open("Tests/images/apng/delay.png")
im.seek(1)
im.close()
@@ -674,7 +679,10 @@ def test_seek_after_close():
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
@pytest.mark.parametrize("default_image", (True, False))
-def test_different_modes_in_later_frames(mode, default_image, tmp_path):
+@pytest.mark.parametrize("duplicate", (True, False))
+def test_different_modes_in_later_frames(
+ mode, default_image, duplicate, tmp_path: Path
+) -> None:
test_file = str(tmp_path / "temp.png")
im = Image.new("L", (1, 1))
@@ -682,7 +690,16 @@ def test_different_modes_in_later_frames(mode, default_image, tmp_path):
test_file,
save_all=True,
default_image=default_image,
- append_images=[Image.new(mode, (1, 1))],
+ append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],
)
with Image.open(test_file) as reloaded:
assert reloaded.mode == mode
+
+
+def test_apng_repeated_seeks_give_correct_info() -> None:
+ with Image.open("Tests/images/apng/different_durations.png") as im:
+ for i in range(3):
+ im.seek(0)
+ assert im.info["duration"] == 4000
+ im.seek(1)
+ assert im.info["duration"] == 1000
diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py
index 8b1355b62..3904d3bc5 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -10,7 +14,7 @@ from .helper import (
)
-def test_load_blp1():
+def test_load_blp1() -> None:
with Image.open("Tests/images/blp/blp1_jpeg.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
@@ -18,22 +22,22 @@ def test_load_blp1():
im.load()
-def test_load_blp2_raw():
+def test_load_blp2_raw() -> None:
with Image.open("Tests/images/blp/blp2_raw.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png")
-def test_load_blp2_dxt1():
+def test_load_blp2_dxt1() -> None:
with Image.open("Tests/images/blp/blp2_dxt1.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png")
-def test_load_blp2_dxt1a():
+def test_load_blp2_dxt1a() -> None:
with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
-def test_save(tmp_path):
+def test_save(tmp_path: Path) -> None:
f = str(tmp_path / "temp.blp")
for version in ("BLP1", "BLP2"):
@@ -67,7 +71,7 @@ def test_save(tmp_path):
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
],
)
-def test_crashes(test_file):
+def test_crashes(test_file) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(OSError):
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index 9e79937e9..c36466e02 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
import io
+from pathlib import Path
import pytest
@@ -12,8 +15,8 @@ from .helper import (
)
-def test_sanity(tmp_path):
- def roundtrip(im):
+def test_sanity(tmp_path: Path) -> None:
+ def roundtrip(im) -> None:
outfile = str(tmp_path / "temp.bmp")
im.save(outfile, "BMP")
@@ -33,20 +36,20 @@ def test_sanity(tmp_path):
roundtrip(hopper("RGB"))
-def test_invalid_file():
+def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):
BmpImagePlugin.BmpImageFile(fp)
-def test_fallback_if_mmap_errors():
+def test_fallback_if_mmap_errors() -> None:
# This image has been truncated,
# so that the buffer is not large enough when using mmap
with Image.open("Tests/images/mmap_error.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp")
-def test_save_to_bytes():
+def test_save_to_bytes() -> None:
output = io.BytesIO()
im = hopper()
im.save(output, "BMP")
@@ -58,7 +61,7 @@ def test_save_to_bytes():
assert reloaded.format == "BMP"
-def test_small_palette(tmp_path):
+def test_small_palette(tmp_path: Path) -> None:
im = Image.new("P", (1, 1))
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
im.putpalette(colors)
@@ -70,7 +73,7 @@ def test_small_palette(tmp_path):
assert reloaded.getpalette() == colors
-def test_save_too_large(tmp_path):
+def test_save_too_large(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.bmp")
with Image.new("RGB", (1, 1)) as im:
im._size = (37838, 37838)
@@ -78,7 +81,7 @@ def test_save_too_large(tmp_path):
im.save(outfile)
-def test_dpi():
+def test_dpi() -> None:
dpi = (72, 72)
output = io.BytesIO()
@@ -90,7 +93,7 @@ def test_dpi():
assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161)
-def test_save_bmp_with_dpi(tmp_path):
+def test_save_bmp_with_dpi(tmp_path: Path) -> None:
# Test for #1301
# Arrange
outfile = str(tmp_path / "temp.jpg")
@@ -108,7 +111,7 @@ def test_save_bmp_with_dpi(tmp_path):
assert reloaded.format == "JPEG"
-def test_save_float_dpi(tmp_path):
+def test_save_float_dpi(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.bmp")
with Image.open("Tests/images/hopper.bmp") as im:
im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
@@ -116,7 +119,7 @@ def test_save_float_dpi(tmp_path):
assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306)
-def test_load_dib():
+def test_load_dib() -> None:
# test for #1293, Imagegrab returning Unsupported Bitfields Format
with Image.open("Tests/images/clipboard.dib") as im:
assert im.format == "DIB"
@@ -125,7 +128,7 @@ def test_load_dib():
assert_image_equal_tofile(im, "Tests/images/clipboard_target.png")
-def test_save_dib(tmp_path):
+def test_save_dib(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.dib")
with Image.open("Tests/images/clipboard.dib") as im:
@@ -137,7 +140,7 @@ def test_save_dib(tmp_path):
assert_image_equal(im, reloaded)
-def test_rgba_bitfields():
+def test_rgba_bitfields() -> None:
# This test image has been manually hexedited
# to change the bitfield compression in the header from XBGR to RGBA
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
@@ -155,11 +158,11 @@ def test_rgba_bitfields():
)
-def test_rle8():
+def test_rle8() -> None:
with Image.open("Tests/images/hopper_rle8.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
- with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im:
+ with Image.open("Tests/images/hopper_rle8_grayscale.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
# This test image has been manually hexedited
@@ -175,7 +178,7 @@ def test_rle8():
im.load()
-def test_rle4():
+def test_rle4() -> None:
with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im:
assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12)
@@ -191,7 +194,7 @@ def test_rle4():
("Tests/images/bmp/g/pal8rle.bmp", 1064),
),
)
-def test_rle8_eof(file_name, length):
+def test_rle8_eof(file_name, length) -> None:
with open(file_name, "rb") as fp:
data = fp.read(length)
with Image.open(io.BytesIO(data)) as im:
@@ -199,7 +202,7 @@ def test_rle8_eof(file_name, length):
im.load()
-def test_offset():
+def test_offset() -> None:
# This image has been hexedited
# to exclude the palette size from the pixel data offset
with Image.open("Tests/images/pal8_offset.bmp") as im:
diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py
index a7714c92c..3dd24533a 100644
--- a/Tests/test_file_bufrstub.py
+++ b/Tests/test_file_bufrstub.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import BufrStubImagePlugin, Image
@@ -7,7 +11,7 @@ from .helper import hopper
TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d"
-def test_open():
+def test_open() -> None:
# Act
with Image.open(TEST_FILE) as im:
# Assert
@@ -18,7 +22,7 @@ def test_open():
assert im.size == (1, 1)
-def test_invalid_file():
+def test_invalid_file() -> None:
# Arrange
invalid_file = "Tests/images/flower.jpg"
@@ -27,7 +31,7 @@ def test_invalid_file():
BufrStubImagePlugin.BufrStubImageFile(invalid_file)
-def test_load():
+def test_load() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler
@@ -35,7 +39,7 @@ def test_load():
im.load()
-def test_save(tmp_path):
+def test_save(tmp_path: Path) -> None:
# Arrange
im = hopper()
tmpfile = str(tmp_path / "temp.bufr")
@@ -45,13 +49,13 @@ def test_save(tmp_path):
im.save(tmpfile)
-def test_handler(tmp_path):
+def test_handler(tmp_path: Path) -> None:
class TestHandler:
opened = False
loaded = False
saved = False
- def open(self, im):
+ def open(self, im) -> None:
self.opened = True
def load(self, im):
@@ -59,7 +63,7 @@ def test_handler(tmp_path):
im.fp.close()
return Image.new("RGB", (1, 1))
- def save(self, im, fp, filename):
+ def save(self, im, fp, filename) -> None:
self.saved = True
handler = TestHandler()
diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py
index 65cf6a75e..4dba4be5d 100644
--- a/Tests/test_file_container.py
+++ b/Tests/test_file_container.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import ContainerIO, Image
@@ -7,19 +9,19 @@ from .helper import hopper
TEST_FILE = "Tests/images/dummy.container"
-def test_sanity():
+def test_sanity() -> None:
dir(Image)
dir(ContainerIO)
-def test_isatty():
+def test_isatty() -> None:
with hopper() as im:
container = ContainerIO.ContainerIO(im, 0, 0)
assert container.isatty() is False
-def test_seek_mode_0():
+def test_seek_mode_0() -> None:
# Arrange
mode = 0
with open(TEST_FILE, "rb") as fh:
@@ -33,7 +35,7 @@ def test_seek_mode_0():
assert container.tell() == 33
-def test_seek_mode_1():
+def test_seek_mode_1() -> None:
# Arrange
mode = 1
with open(TEST_FILE, "rb") as fh:
@@ -47,7 +49,7 @@ def test_seek_mode_1():
assert container.tell() == 66
-def test_seek_mode_2():
+def test_seek_mode_2() -> None:
# Arrange
mode = 2
with open(TEST_FILE, "rb") as fh:
@@ -62,7 +64,7 @@ def test_seek_mode_2():
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_read_n0(bytesmode):
+def test_read_n0(bytesmode) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@@ -78,7 +80,7 @@ def test_read_n0(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_read_n(bytesmode):
+def test_read_n(bytesmode) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@@ -94,7 +96,7 @@ def test_read_n(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_read_eof(bytesmode):
+def test_read_eof(bytesmode) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@@ -110,7 +112,7 @@ def test_read_eof(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_readline(bytesmode):
+def test_readline(bytesmode) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
@@ -125,7 +127,7 @@ def test_readline(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_readlines(bytesmode):
+def test_readlines(bytesmode) -> None:
# Arrange
expected = [
"This is line 1\n",
diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py
index f04a20a22..dbf1b866d 100644
--- a/Tests/test_file_cur.py
+++ b/Tests/test_file_cur.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import CurImagePlugin, Image
@@ -5,7 +7,7 @@ from PIL import CurImagePlugin, Image
TEST_FILE = "Tests/images/deerstalker.cur"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(TEST_FILE) as im:
assert im.size == (32, 32)
assert isinstance(im, CurImagePlugin.CurImageFile)
@@ -15,7 +17,7 @@ def test_sanity():
assert im.getpixel((16, 16)) == (84, 87, 86, 255)
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py
index 22686af34..65337cad9 100644
--- a/Tests/test_file_dcx.py
+++ b/Tests/test_file_dcx.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import warnings
import pytest
@@ -10,7 +12,7 @@ from .helper import assert_image_equal, hopper, is_pypy
TEST_FILE = "Tests/images/hopper.dcx"
-def test_sanity():
+def test_sanity() -> None:
# Arrange
# Act
@@ -23,8 +25,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
-def test_unclosed_file():
- def open():
+def test_unclosed_file() -> None:
+ def open() -> None:
im = Image.open(TEST_FILE)
im.load()
@@ -32,26 +34,26 @@ def test_unclosed_file():
open()
-def test_closed_file():
+def test_closed_file() -> None:
with warnings.catch_warnings():
im = Image.open(TEST_FILE)
im.load()
im.close()
-def test_context_manager():
+def test_context_manager() -> None:
with warnings.catch_warnings():
with Image.open(TEST_FILE) as im:
im.load()
-def test_invalid_file():
+def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):
DcxImagePlugin.DcxImageFile(fp)
-def test_tell():
+def test_tell() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
# Act
@@ -61,13 +63,13 @@ def test_tell():
assert frame == 0
-def test_n_frames():
+def test_n_frames() -> None:
with Image.open(TEST_FILE) as im:
assert im.n_frames == 1
assert not im.is_animated
-def test_eoferror():
+def test_eoferror() -> None:
with Image.open(TEST_FILE) as im:
n_frames = im.n_frames
@@ -80,7 +82,7 @@ def test_eoferror():
im.seek(n_frames - 1)
-def test_seek_too_far():
+def test_seek_too_far() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
frame = 999 # too big on purpose
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index bb9af7967..09ee8986a 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -1,5 +1,8 @@
"""Test DdsImagePlugin"""
+from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
import pytest
@@ -12,9 +15,14 @@ TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
TEST_FILE_ATI1 = "Tests/images/ati1.dds"
TEST_FILE_ATI2 = "Tests/images/ati2.dds"
+TEST_FILE_DX10_BC4_TYPELESS = "Tests/images/bc4_typeless.dds"
+TEST_FILE_DX10_BC4_UNORM = "Tests/images/bc4_unorm.dds"
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
+TEST_FILE_DX10_BC1 = "Tests/images/bc1.dds"
+TEST_FILE_DX10_BC1_TYPELESS = "Tests/images/bc1_typeless.dds"
+TEST_FILE_BC4U = "Tests/images/bc4u.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_BC5U = "Tests/images/bc5u.dds"
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
@@ -26,14 +34,24 @@ TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SR
TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
+TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
-def test_sanity_dxt1():
- """Check DXT1 images can be opened"""
+@pytest.mark.parametrize(
+ "image_path",
+ (
+ TEST_FILE_DXT1,
+ # hexeditted to use DX10 FourCC
+ TEST_FILE_DX10_BC1,
+ TEST_FILE_DX10_BC1_TYPELESS,
+ ),
+)
+def test_sanity_dxt1_bc1(image_path) -> None:
+ """Check DXT1 and BC1 images can be opened"""
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
target = target.convert("RGBA")
- with Image.open(TEST_FILE_DXT1) as im:
+ with Image.open(image_path) as im:
im.load()
assert im.format == "DDS"
@@ -43,7 +61,7 @@ def test_sanity_dxt1():
assert_image_equal(im, target)
-def test_sanity_dxt3():
+def test_sanity_dxt3() -> None:
"""Check DXT3 images can be opened"""
with Image.open(TEST_FILE_DXT3) as im:
@@ -56,7 +74,7 @@ def test_sanity_dxt3():
assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png"))
-def test_sanity_dxt5():
+def test_sanity_dxt5() -> None:
"""Check DXT5 images can be opened"""
with Image.open(TEST_FILE_DXT5) as im:
@@ -69,10 +87,18 @@ def test_sanity_dxt5():
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
-def test_sanity_ati1():
- """Check ATI1 images can be opened"""
+@pytest.mark.parametrize(
+ "image_path",
+ (
+ TEST_FILE_ATI1,
+ # hexeditted to use BC4U FourCC
+ TEST_FILE_BC4U,
+ ),
+)
+def test_sanity_ati1_bc4u(image_path) -> None:
+ """Check ATI1 and BC4U images can be opened"""
- with Image.open(TEST_FILE_ATI1) as im:
+ with Image.open(image_path) as im:
im.load()
assert im.format == "DDS"
@@ -82,6 +108,27 @@ def test_sanity_ati1():
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
+@pytest.mark.parametrize(
+ "image_path",
+ (
+ TEST_FILE_DX10_BC4_UNORM,
+ # hexeditted to be typeless
+ TEST_FILE_DX10_BC4_TYPELESS,
+ ),
+)
+def test_dx10_bc4(image_path) -> None:
+ """Check DX10 BC4 images can be opened"""
+
+ with Image.open(image_path) as im:
+ im.load()
+
+ assert im.format == "DDS"
+ assert im.mode == "L"
+ assert im.size == (64, 64)
+
+ assert_image_equal_tofile(im, TEST_FILE_DX10_BC4_UNORM.replace(".dds", ".png"))
+
+
@pytest.mark.parametrize(
"image_path",
(
@@ -90,7 +137,7 @@ def test_sanity_ati1():
TEST_FILE_BC5U,
),
)
-def test_sanity_ati2_bc5u(image_path):
+def test_sanity_ati2_bc5u(image_path) -> None:
"""Check ATI2 and BC5U images can be opened"""
with Image.open(image_path) as im:
@@ -114,7 +161,7 @@ def test_sanity_ati2_bc5u(image_path):
(TEST_FILE_BC5S, TEST_FILE_BC5S),
),
)
-def test_dx10_bc5(image_path, expected_path):
+def test_dx10_bc5(image_path, expected_path) -> None:
"""Check DX10 BC5 images can be opened"""
with Image.open(image_path) as im:
@@ -128,7 +175,7 @@ def test_dx10_bc5(image_path, expected_path):
@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS))
-def test_dx10_bc6h(image_path):
+def test_dx10_bc6h(image_path) -> None:
"""Check DX10 BC6H/BC6HS images can be opened"""
with Image.open(image_path) as im:
@@ -141,7 +188,7 @@ def test_dx10_bc6h(image_path):
assert_image_equal_tofile(im, image_path.replace(".dds", ".png"))
-def test_dx10_bc7():
+def test_dx10_bc7() -> None:
"""Check DX10 images can be opened"""
with Image.open(TEST_FILE_DX10_BC7) as im:
@@ -154,7 +201,7 @@ def test_dx10_bc7():
assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png"))
-def test_dx10_bc7_unorm_srgb():
+def test_dx10_bc7_unorm_srgb() -> None:
"""Check DX10 unsigned normalized integer images can be opened"""
with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im:
@@ -170,7 +217,7 @@ def test_dx10_bc7_unorm_srgb():
)
-def test_dx10_r8g8b8a8():
+def test_dx10_r8g8b8a8() -> None:
"""Check DX10 images can be opened"""
with Image.open(TEST_FILE_DX10_R8G8B8A8) as im:
@@ -183,7 +230,7 @@ def test_dx10_r8g8b8a8():
assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png"))
-def test_dx10_r8g8b8a8_unorm_srgb():
+def test_dx10_r8g8b8a8_unorm_srgb() -> None:
"""Check DX10 unsigned normalized integer images can be opened"""
with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im:
@@ -199,22 +246,17 @@ def test_dx10_r8g8b8a8_unorm_srgb():
)
-def test_unimplemented_dxgi_format():
- with pytest.raises(NotImplementedError):
- with Image.open("Tests/images/unimplemented_dxgi_format.dds"):
- pass
-
-
@pytest.mark.parametrize(
("mode", "size", "test_file"),
[
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
+ ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15),
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
],
)
-def test_uncompressed(mode, size, test_file):
+def test_uncompressed(mode, size, test_file) -> None:
"""Check uncompressed images can be opened"""
with Image.open(test_file) as im:
@@ -225,7 +267,7 @@ def test_uncompressed(mode, size, test_file):
assert_image_equal_tofile(im, test_file.replace(".dds", ".png"))
-def test__accept_true():
+def test__accept_true() -> None:
"""Check valid prefix"""
# Arrange
prefix = b"DDS etc"
@@ -237,7 +279,7 @@ def test__accept_true():
assert output
-def test__accept_false():
+def test__accept_false() -> None:
"""Check invalid prefix"""
# Arrange
prefix = b"something invalid"
@@ -249,19 +291,19 @@ def test__accept_false():
assert not output
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
DdsImagePlugin.DdsImageFile(invalid_file)
-def test_short_header():
+def test_short_header() -> None:
"""Check a short header"""
with open(TEST_FILE_DXT5, "rb") as f:
img_file = f.read()
- def short_header():
+ def short_header() -> None:
with Image.open(BytesIO(img_file[:119])):
pass # pragma: no cover
@@ -269,13 +311,13 @@ def test_short_header():
short_header()
-def test_short_file():
+def test_short_file() -> None:
"""Check that the appropriate error is thrown for a short file"""
with open(TEST_FILE_DXT5, "rb") as f:
img_file = f.read()
- def short_file():
+ def short_file() -> None:
with Image.open(BytesIO(img_file[:-100])) as im:
im.load()
@@ -283,7 +325,7 @@ def test_short_file():
short_file()
-def test_dxt5_colorblock_alpha_issue_4142():
+def test_dxt5_colorblock_alpha_issue_4142() -> None:
"""Check that colorblocks are decoded correctly in DXT5"""
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
@@ -298,18 +340,31 @@ def test_dxt5_colorblock_alpha_issue_4142():
assert px[2] != 0
-def test_palette():
+def test_palette() -> None:
with Image.open("Tests/images/palette.dds") as im:
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
-def test_unimplemented_pixel_format():
- with pytest.raises(NotImplementedError):
- with Image.open("Tests/images/unimplemented_pixel_format.dds"):
+def test_unsupported_bitcount() -> None:
+ with pytest.raises(OSError):
+ with Image.open("Tests/images/unsupported_bitcount.dds"):
pass
-def test_save_unsupported_mode(tmp_path):
+@pytest.mark.parametrize(
+ "test_file",
+ (
+ "Tests/images/unimplemented_dxgi_format.dds",
+ "Tests/images/unimplemented_pfflags.dds",
+ ),
+)
+def test_not_implemented(test_file) -> None:
+ with pytest.raises(NotImplementedError):
+ with Image.open(test_file):
+ pass
+
+
+def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds")
im = hopper("HSV")
with pytest.raises(OSError):
@@ -325,7 +380,7 @@ def test_save_unsupported_mode(tmp_path):
("RGBA", "Tests/images/pil123rgba.png"),
],
)
-def test_save(mode, test_file, tmp_path):
+def test_save(mode, test_file, tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds")
with Image.open(test_file) as im:
assert im.mode == mode
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index 259cf75c3..06f927c7b 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
import io
+from pathlib import Path
import pytest
@@ -81,7 +84,7 @@ simple_eps_file_with_long_binary_data = (
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
)
@pytest.mark.parametrize("scale", (1, 2))
-def test_sanity(filename, size, scale):
+def test_sanity(filename, size, scale) -> None:
expected_size = tuple(s * scale for s in size)
with Image.open(filename) as image:
image.load(scale=scale)
@@ -91,7 +94,7 @@ def test_sanity(filename, size, scale):
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_load():
+def test_load() -> None:
with Image.open(FILE1) as im:
assert im.load()[0, 0] == (255, 255, 255)
@@ -99,7 +102,7 @@ def test_load():
assert im.load()[0, 0] == (255, 255, 255)
-def test_binary():
+def test_binary() -> None:
if HAS_GHOSTSCRIPT:
assert EpsImagePlugin.gs_binary is not None
else:
@@ -113,41 +116,41 @@ def test_binary():
assert EpsImagePlugin.gs_windows_binary is not None
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
EpsImagePlugin.EpsImageFile(invalid_file)
-def test_binary_header_only():
+def test_binary_header_only() -> None:
data = io.BytesIO(simple_binary_header)
with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'):
EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
-def test_missing_version_comment(prefix):
+def test_missing_version_comment(prefix) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
with pytest.raises(SyntaxError):
EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
-def test_missing_boundingbox_comment(prefix):
+def test_missing_boundingbox_comment(prefix) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox))
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
-def test_invalid_boundingbox_comment(prefix):
+def test_invalid_boundingbox_comment(prefix) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
-def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix):
+def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None:
data = io.BytesIO(
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
)
@@ -158,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix):
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
-def test_ascii_comment_too_long(prefix):
+def test_ascii_comment_too_long(prefix) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
with pytest.raises(SyntaxError, match="not an EPS file"):
EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
-def test_long_binary_data(prefix):
+def test_long_binary_data(prefix) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
EpsImagePlugin.EpsImageFile(data)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
-def test_load_long_binary_data(prefix):
+def test_load_long_binary_data(prefix) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
with Image.open(data) as img:
img.load()
@@ -185,7 +188,7 @@ def test_load_long_binary_data(prefix):
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_cmyk():
+def test_cmyk() -> None:
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
assert cmyk_image.mode == "CMYK"
assert cmyk_image.size == (100, 100)
@@ -201,7 +204,7 @@ def test_cmyk():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_showpage():
+def test_showpage() -> None:
# See https://github.com/python-pillow/Pillow/issues/2615
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
with Image.open("Tests/images/reqd_showpage.png") as target:
@@ -212,7 +215,7 @@ def test_showpage():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_transparency():
+def test_transparency() -> None:
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
plot_image.load(transparency=True)
assert plot_image.mode == "RGBA"
@@ -223,7 +226,7 @@ def test_transparency():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_file_object(tmp_path):
+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:
@@ -231,7 +234,7 @@ def test_file_object(tmp_path):
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_bytesio_object():
+def test_bytesio_object() -> None:
with open(FILE1, "rb") as f:
img_bytes = io.BytesIO(f.read())
@@ -244,12 +247,12 @@ def test_bytesio_object():
assert_image_similar(img, image1_scale1_compare, 5)
-def test_1_mode():
+def test_1_mode() -> None:
with Image.open("Tests/images/1.eps") as im:
assert im.mode == "1"
-def test_image_mode_not_supported(tmp_path):
+def test_image_mode_not_supported(tmp_path: Path) -> None:
im = hopper("RGBA")
tmpfile = str(tmp_path / "temp.eps")
with pytest.raises(ValueError):
@@ -258,7 +261,7 @@ def test_image_mode_not_supported(tmp_path):
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@skip_unless_feature("zlib")
-def test_render_scale1():
+def test_render_scale1() -> None:
# We need png support for these render test
# Zero bounding box
@@ -269,7 +272,7 @@ def test_render_scale1():
image1_scale1_compare.load()
assert_image_similar(image1_scale1, image1_scale1_compare, 5)
- # Non-Zero bounding box
+ # Non-zero bounding box
with Image.open(FILE2) as image2_scale1:
image2_scale1.load()
with Image.open(FILE2_COMPARE) as image2_scale1_compare:
@@ -280,7 +283,7 @@ def test_render_scale1():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@skip_unless_feature("zlib")
-def test_render_scale2():
+def test_render_scale2() -> None:
# We need png support for these render test
# Zero bounding box
@@ -291,7 +294,7 @@ def test_render_scale2():
image1_scale2_compare.load()
assert_image_similar(image1_scale2, image1_scale2_compare, 5)
- # Non-Zero bounding box
+ # Non-zero bounding box
with Image.open(FILE2) as image2_scale2:
image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
@@ -302,7 +305,7 @@ def test_render_scale2():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
-def test_resize(filename):
+def test_resize(filename) -> None:
with Image.open(filename) as im:
new_size = (100, 100)
im = im.resize(new_size)
@@ -311,7 +314,7 @@ def test_resize(filename):
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("filename", (FILE1, FILE2))
-def test_thumbnail(filename):
+def test_thumbnail(filename) -> None:
# Issue #619
with Image.open(filename) as im:
new_size = (100, 100)
@@ -319,20 +322,20 @@ def test_thumbnail(filename):
assert max(im.size) == max(new_size)
-def test_read_binary_preview():
+def test_read_binary_preview() -> None:
# Issue 302
# open image with binary preview
with Image.open(FILE3):
pass
-def test_readline_psfile(tmp_path):
+def test_readline_psfile(tmp_path: Path) -> None:
# check all the freaking line endings possible from the spec
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
line_endings = ["\r\n", "\n", "\n\r", "\r"]
strings = ["something", "else", "baz", "bif"]
- def _test_readline(t, ending):
+ def _test_readline(t, ending) -> None:
ending = "Failure with line ending: %s" % (
"".join("%s" % ord(s) for s in ending)
)
@@ -341,13 +344,13 @@ def test_readline_psfile(tmp_path):
assert t.readline().strip("\r\n") == "baz", ending
assert t.readline().strip("\r\n") == "bif", ending
- def _test_readline_io_psfile(test_string, ending):
+ def _test_readline_io_psfile(test_string, ending) -> None:
f = io.BytesIO(test_string.encode("latin-1"))
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(f)
_test_readline(t, ending)
- def _test_readline_file_psfile(test_string, ending):
+ def _test_readline_file_psfile(test_string, ending) -> None:
f = str(tmp_path / "temp.txt")
with open(f, "wb") as w:
w.write(test_string.encode("latin-1"))
@@ -363,7 +366,7 @@ def test_readline_psfile(tmp_path):
_test_readline_file_psfile(s, ending)
-def test_psfile_deprecation():
+def test_psfile_deprecation() -> None:
with pytest.warns(DeprecationWarning):
EpsImagePlugin.PSFile(None)
@@ -373,7 +376,7 @@ def test_psfile_deprecation():
"line_ending",
(b"\r\n", b"\n", b"\n\r", b"\r"),
)
-def test_readline(prefix, line_ending):
+def test_readline(prefix, line_ending) -> None:
simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
data = io.BytesIO(simple_file)
test_file = EpsImagePlugin.EpsImageFile(data)
@@ -391,14 +394,14 @@ def test_readline(prefix, line_ending):
"Tests/images/illuCS6_preview.eps",
),
)
-def test_open_eps(filename):
+def test_open_eps(filename) -> None:
# https://github.com/python-pillow/Pillow/issues/1104
with Image.open(filename) as img:
assert img.mode == "RGB"
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_emptyline():
+def test_emptyline() -> None:
# Test file includes an empty line in the header data
emptyline_file = "Tests/images/zero_bb_emptyline.eps"
@@ -414,14 +417,14 @@ def test_emptyline():
"test_file",
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
)
-def test_timeout(test_file):
+def test_timeout(test_file) -> None:
with open(test_file, "rb") as f:
with pytest.raises(Image.UnidentifiedImageError):
with Image.open(f):
pass
-def test_bounding_box_in_trailer():
+def test_bounding_box_in_trailer() -> None:
# Check bounding boxes are parsed in the same way
# when specified in the header and the trailer
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
@@ -430,7 +433,7 @@ def test_bounding_box_in_trailer():
assert trailer_image.size == header_image.size
-def test_eof_before_bounding_box():
+def test_eof_before_bounding_box() -> None:
with pytest.raises(OSError):
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
pass
diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py
index 68b3eb567..cce0b05cd 100644
--- a/Tests/test_file_fits.py
+++ b/Tests/test_file_fits.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -9,7 +11,7 @@ from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/hopper.fits"
-def test_open():
+def test_open() -> None:
# Act
with Image.open(TEST_FILE) as im:
# Assert
@@ -20,7 +22,7 @@ def test_open():
assert_image_equal(im, hopper("L"))
-def test_invalid_file():
+def test_invalid_file() -> None:
# Arrange
invalid_file = "Tests/images/flower.jpg"
@@ -29,14 +31,14 @@ def test_invalid_file():
FitsImagePlugin.FitsImageFile(invalid_file)
-def test_truncated_fits():
+def test_truncated_fits() -> None:
# No END to headers
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
with pytest.raises(OSError):
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
-def test_naxis_zero():
+def test_naxis_zero() -> None:
# This test image has been manually hexedited
# to set the number of data axes to zero
with pytest.raises(ValueError):
@@ -44,7 +46,7 @@ def test_naxis_zero():
pass
-def test_comment():
+def test_comment() -> None:
image_data = b"SIMPLE = T / comment string"
with pytest.raises(OSError):
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index f96afdc95..a673d4af8 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import warnings
import pytest
@@ -14,7 +16,7 @@ static_test_file = "Tests/images/hopper.fli"
animated_test_file = "Tests/images/a.fli"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(static_test_file) as im:
im.load()
assert im.mode == "P"
@@ -31,8 +33,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
-def test_unclosed_file():
- def open():
+def test_unclosed_file() -> None:
+ def open() -> None:
im = Image.open(static_test_file)
im.load()
@@ -40,14 +42,14 @@ def test_unclosed_file():
open()
-def test_closed_file():
+def test_closed_file() -> None:
with warnings.catch_warnings():
im = Image.open(static_test_file)
im.load()
im.close()
-def test_seek_after_close():
+def test_seek_after_close() -> None:
im = Image.open(animated_test_file)
im.seek(1)
im.close()
@@ -56,13 +58,13 @@ def test_seek_after_close():
im.seek(0)
-def test_context_manager():
+def test_context_manager() -> None:
with warnings.catch_warnings():
with Image.open(static_test_file) as im:
im.load()
-def test_tell():
+def test_tell() -> None:
# Arrange
with Image.open(static_test_file) as im:
# Act
@@ -72,20 +74,20 @@ def test_tell():
assert frame == 0
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
FliImagePlugin.FliImageFile(invalid_file)
-def test_palette_chunk_second():
+def test_palette_chunk_second() -> None:
with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im:
with Image.open(static_test_file) as expected:
assert_image_equal(im.convert("RGB"), expected.convert("RGB"))
-def test_n_frames():
+def test_n_frames() -> None:
with Image.open(static_test_file) as im:
assert im.n_frames == 1
assert not im.is_animated
@@ -95,7 +97,7 @@ def test_n_frames():
assert im.is_animated
-def test_eoferror():
+def test_eoferror() -> None:
with Image.open(animated_test_file) as im:
n_frames = im.n_frames
@@ -108,7 +110,7 @@ def test_eoferror():
im.seek(n_frames - 1)
-def test_seek_tell():
+def test_seek_tell() -> None:
with Image.open(animated_test_file) as im:
layer_number = im.tell()
assert layer_number == 0
@@ -130,7 +132,7 @@ def test_seek_tell():
assert layer_number == 1
-def test_seek():
+def test_seek() -> None:
with Image.open(animated_test_file) as im:
im.seek(50)
@@ -145,7 +147,7 @@ def test_seek():
],
)
@pytest.mark.timeout(timeout=3)
-def test_timeouts(test_file):
+def test_timeouts(test_file) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(OSError):
@@ -158,7 +160,7 @@ def test_timeouts(test_file):
"Tests/images/crash-5762152299364352.fli",
],
)
-def test_crash(test_file):
+def test_crash(test_file) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(OSError):
diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py
index 9a1784d31..e32f30a01 100644
--- a/Tests/test_file_fpx.py
+++ b/Tests/test_file_fpx.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -9,7 +11,7 @@ FpxImagePlugin = pytest.importorskip(
)
-def test_sanity():
+def test_sanity() -> None:
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
assert im.mode == "L"
assert im.size == (70, 46)
@@ -18,7 +20,7 @@ def test_sanity():
assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png")
-def test_close():
+def test_close() -> None:
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
pass
assert im.ole.fp.closed
@@ -28,7 +30,7 @@ def test_close():
assert im.ole.fp.closed
-def test_invalid_file():
+def test_invalid_file() -> None:
# Test an invalid OLE file
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
@@ -40,7 +42,7 @@ def test_invalid_file():
FpxImagePlugin.FpxImageFile(ole_file)
-def test_fpx_invalid_number_of_bands():
+def test_fpx_invalid_number_of_bands() -> None:
with pytest.raises(OSError, match="Invalid number of bands"):
with Image.open("Tests/images/input_bw_five_bands.fpx"):
pass
diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py
index ac6253db0..0c544245a 100644
--- a/Tests/test_file_ftex.py
+++ b/Tests/test_file_ftex.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import FtexImagePlugin, Image
@@ -5,18 +7,18 @@ from PIL import FtexImagePlugin, Image
from .helper import assert_image_equal_tofile, assert_image_similar
-def test_load_raw():
+def test_load_raw() -> None:
with Image.open("Tests/images/ftex_uncompressed.ftu") as im:
assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png")
-def test_load_dxt1():
+def test_load_dxt1() -> None:
with Image.open("Tests/images/ftex_dxt1.ftc") as im:
with Image.open("Tests/images/ftex_dxt1.png") as target:
assert_image_similar(im, target.convert("RGBA"), 15)
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py
index 1ea8af8ee..be98b08f2 100644
--- a/Tests/test_file_gbr.py
+++ b/Tests/test_file_gbr.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import GbrImagePlugin, Image
@@ -5,12 +7,12 @@ from PIL import GbrImagePlugin, Image
from .helper import assert_image_equal_tofile
-def test_gbr_file():
+def test_gbr_file() -> None:
with Image.open("Tests/images/gbr.gbr") as im:
assert_image_equal_tofile(im, "Tests/images/gbr.png")
-def test_load():
+def test_load() -> None:
with Image.open("Tests/images/gbr.gbr") as im:
assert im.load()[0, 0] == (0, 0, 0, 0)
@@ -18,14 +20,14 @@ def test_load():
assert im.load()[0, 0] == (0, 0, 0, 0)
-def test_multiple_load_operations():
+def test_multiple_load_operations() -> None:
with Image.open("Tests/images/gbr.gbr") as im:
im.load()
im.load()
assert_image_equal_tofile(im, "Tests/images/gbr.png")
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py
index 5594e5bbb..d512df284 100644
--- a/Tests/test_file_gd.py
+++ b/Tests/test_file_gd.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import GdImageFile, UnidentifiedImageError
@@ -5,18 +7,18 @@ from PIL import GdImageFile, UnidentifiedImageError
TEST_GD_FILE = "Tests/images/hopper.gd"
-def test_sanity():
+def test_sanity() -> None:
with GdImageFile.open(TEST_GD_FILE) as im:
assert im.size == (128, 128)
assert im.format == "GD"
-def test_bad_mode():
+def test_bad_mode() -> None:
with pytest.raises(ValueError):
GdImageFile.open(TEST_GD_FILE, "bad mode")
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(UnidentifiedImageError):
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 3c2e96356..3f550fd11 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
import warnings
from io import BytesIO
+from pathlib import Path
import pytest
@@ -21,7 +24,7 @@ with open(TEST_GIF, "rb") as f:
data = f.read()
-def test_sanity():
+def test_sanity() -> None:
with Image.open(TEST_GIF) as im:
im.load()
assert im.mode == "P"
@@ -31,8 +34,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
-def test_unclosed_file():
- def open():
+def test_unclosed_file() -> None:
+ def open() -> None:
im = Image.open(TEST_GIF)
im.load()
@@ -40,14 +43,14 @@ def test_unclosed_file():
open()
-def test_closed_file():
+def test_closed_file() -> None:
with warnings.catch_warnings():
im = Image.open(TEST_GIF)
im.load()
im.close()
-def test_seek_after_close():
+def test_seek_after_close() -> None:
im = Image.open("Tests/images/iss634.gif")
im.load()
im.close()
@@ -60,20 +63,20 @@ def test_seek_after_close():
im.seek(1)
-def test_context_manager():
+def test_context_manager() -> None:
with warnings.catch_warnings():
with Image.open(TEST_GIF) as im:
im.load()
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
GifImagePlugin.GifImageFile(invalid_file)
-def test_l_mode_transparency():
+def test_l_mode_transparency() -> None:
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
assert im.mode == "L"
assert im.load()[0, 0] == 128
@@ -84,7 +87,7 @@ def test_l_mode_transparency():
assert im.load()[0, 0] == 128
-def test_l_mode_after_rgb():
+def test_l_mode_after_rgb() -> None:
with Image.open("Tests/images/no_palette_after_rgb.gif") as im:
im.seek(1)
assert im.mode == "RGB"
@@ -93,13 +96,13 @@ def test_l_mode_after_rgb():
assert im.mode == "RGB"
-def test_palette_not_needed_for_second_frame():
+def test_palette_not_needed_for_second_frame() -> None:
with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im:
im.seek(1)
assert_image_similar(im, hopper("L").convert("RGB"), 8)
-def test_strategy():
+def test_strategy() -> None:
with Image.open("Tests/images/iss634.gif") as im:
expected_rgb_always = im.convert("RGB")
@@ -140,7 +143,7 @@ def test_strategy():
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
-def test_optimize():
+def test_optimize() -> None:
def test_grayscale(optimize):
im = Image.new("L", (1, 1), 0)
filename = BytesIO()
@@ -175,7 +178,7 @@ def test_optimize():
(4, 513, 256),
),
)
-def test_optimize_correctness(colors, size, expected_palette_length):
+def test_optimize_correctness(colors, size, expected_palette_length) -> None:
# 256 color Palette image, posterize to > 128 and < 128 levels.
# Size bigger and smaller than 512x512.
# Check the palette for number of colors allocated.
@@ -197,14 +200,14 @@ def test_optimize_correctness(colors, size, expected_palette_length):
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
-def test_optimize_full_l():
+def test_optimize_full_l() -> None:
im = Image.frombytes("L", (16, 16), bytes(range(256)))
test_file = BytesIO()
im.save(test_file, "GIF", optimize=True)
assert im.mode == "L"
-def test_optimize_if_palette_can_be_reduced_by_half():
+def test_optimize_if_palette_can_be_reduced_by_half() -> None:
im = Image.new("P", (8, 1))
im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
for i in range(8):
@@ -217,7 +220,28 @@ def test_optimize_if_palette_can_be_reduced_by_half():
assert len(reloaded.palette.palette) // 3 == colors
-def test_roundtrip(tmp_path):
+def test_full_palette_second_frame(tmp_path: Path) -> None:
+ out = str(tmp_path / "temp.gif")
+ im = Image.new("P", (1, 256))
+
+ full_palette_im = Image.new("P", (1, 256))
+ for i in range(256):
+ full_palette_im.putpixel((0, i), i)
+ full_palette_im.palette = ImagePalette.ImagePalette(
+ "RGB", bytearray(i // 3 for i in range(768))
+ )
+ full_palette_im.palette.dirty = 1
+
+ im.save(out, save_all=True, append_images=[full_palette_im])
+
+ with Image.open(out) as reloaded:
+ reloaded.seek(1)
+
+ for i in range(256):
+ reloaded.getpixel((0, i)) == i
+
+
+def test_roundtrip(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = hopper()
im.save(out)
@@ -225,7 +249,7 @@ def test_roundtrip(tmp_path):
assert_image_similar(reread.convert("RGB"), im, 50)
-def test_roundtrip2(tmp_path):
+def test_roundtrip2(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/403
out = str(tmp_path / "temp.gif")
with Image.open(TEST_GIF) as im:
@@ -235,7 +259,7 @@ def test_roundtrip2(tmp_path):
assert_image_similar(reread.convert("RGB"), hopper(), 50)
-def test_roundtrip_save_all(tmp_path):
+def test_roundtrip_save_all(tmp_path: Path) -> None:
# Single frame image
out = str(tmp_path / "temp.gif")
im = hopper()
@@ -252,7 +276,7 @@ def test_roundtrip_save_all(tmp_path):
assert reread.n_frames == 5
-def test_roundtrip_save_all_1(tmp_path):
+def test_roundtrip_save_all_1(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1)
@@ -273,7 +297,7 @@ def test_roundtrip_save_all_1(tmp_path):
("Tests/images/dispose_bgnd_rgba.gif", "RGBA"),
),
)
-def test_loading_multiple_palettes(path, mode):
+def test_loading_multiple_palettes(path, mode) -> None:
with Image.open(path) as im:
assert im.mode == "P"
first_frame_colors = im.palette.colors.keys()
@@ -291,7 +315,7 @@ def test_loading_multiple_palettes(path, mode):
assert im.load()[24, 24] not in first_frame_colors
-def test_headers_saving_for_animated_gifs(tmp_path):
+def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
important_headers = ["background", "version", "duration", "loop"]
# Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
@@ -304,7 +328,7 @@ def test_headers_saving_for_animated_gifs(tmp_path):
assert info[header] == reread.info[header]
-def test_palette_handling(tmp_path):
+def test_palette_handling(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/513
with Image.open(TEST_GIF) as im:
@@ -320,7 +344,7 @@ def test_palette_handling(tmp_path):
assert_image_similar(im, reloaded.convert("RGB"), 10)
-def test_palette_434(tmp_path):
+def test_palette_434(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/434
def roundtrip(im, *args, **kwargs):
@@ -345,7 +369,7 @@ def test_palette_434(tmp_path):
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
-def test_save_netpbm_bmp_mode(tmp_path):
+def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as img:
img = img.convert("RGB")
@@ -356,7 +380,7 @@ def test_save_netpbm_bmp_mode(tmp_path):
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
-def test_save_netpbm_l_mode(tmp_path):
+def test_save_netpbm_l_mode(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as img:
img = img.convert("L")
@@ -366,7 +390,7 @@ def test_save_netpbm_l_mode(tmp_path):
assert_image_similar(img, reloaded.convert("L"), 0)
-def test_seek():
+def test_seek() -> None:
with Image.open("Tests/images/dispose_none.gif") as img:
frame_count = 0
try:
@@ -377,7 +401,7 @@ def test_seek():
assert frame_count == 5
-def test_seek_info():
+def test_seek_info() -> None:
with Image.open("Tests/images/iss634.gif") as im:
info = im.info.copy()
@@ -387,7 +411,7 @@ def test_seek_info():
assert im.info == info
-def test_seek_rewind():
+def test_seek_rewind() -> None:
with Image.open("Tests/images/iss634.gif") as im:
im.seek(2)
im.seek(1)
@@ -405,7 +429,7 @@ def test_seek_rewind():
("Tests/images/iss634.gif", 42),
),
)
-def test_n_frames(path, n_frames):
+def test_n_frames(path, n_frames) -> None:
# Test is_animated before n_frames
with Image.open(path) as im:
assert im.is_animated == (n_frames != 1)
@@ -416,7 +440,7 @@ def test_n_frames(path, n_frames):
assert im.is_animated == (n_frames != 1)
-def test_no_change():
+def test_no_change() -> None:
# Test n_frames does not change the image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(1)
@@ -437,7 +461,7 @@ def test_no_change():
assert_image_equal(im, expected)
-def test_eoferror():
+def test_eoferror() -> None:
with Image.open(TEST_GIF) as im:
n_frames = im.n_frames
@@ -450,13 +474,13 @@ def test_eoferror():
im.seek(n_frames - 1)
-def test_first_frame_transparency():
+def test_first_frame_transparency() -> None:
with Image.open("Tests/images/first_frame_transparency.gif") as im:
px = im.load()
assert px[0, 0] == im.info["transparency"]
-def test_dispose_none():
+def test_dispose_none() -> None:
with Image.open("Tests/images/dispose_none.gif") as img:
try:
while True:
@@ -466,7 +490,7 @@ def test_dispose_none():
pass
-def test_dispose_none_load_end():
+def test_dispose_none_load_end() -> None:
# Test image created with:
#
# im = Image.open("transparent.gif")
@@ -479,7 +503,7 @@ def test_dispose_none_load_end():
assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png")
-def test_dispose_background():
+def test_dispose_background() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as img:
try:
while True:
@@ -489,7 +513,7 @@ def test_dispose_background():
pass
-def test_dispose_background_transparency():
+def test_dispose_background_transparency() -> None:
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
img.seek(2)
px = img.load()
@@ -517,7 +541,7 @@ def test_dispose_background_transparency():
),
),
)
-def test_transparent_dispose(loading_strategy, expected_colors):
+def test_transparent_dispose(loading_strategy, expected_colors) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/transparent_dispose.gif") as img:
@@ -530,7 +554,7 @@ def test_transparent_dispose(loading_strategy, expected_colors):
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
-def test_dispose_previous():
+def test_dispose_previous() -> None:
with Image.open("Tests/images/dispose_prev.gif") as img:
try:
while True:
@@ -540,7 +564,7 @@ def test_dispose_previous():
pass
-def test_dispose_previous_first_frame():
+def test_dispose_previous_first_frame() -> None:
with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
im.seek(1)
assert_image_equal_tofile(
@@ -548,7 +572,7 @@ def test_dispose_previous_first_frame():
)
-def test_previous_frame_loaded():
+def test_previous_frame_loaded() -> None:
with Image.open("Tests/images/dispose_none.gif") as img:
img.load()
img.seek(1)
@@ -559,7 +583,7 @@ def test_previous_frame_loaded():
assert_image_equal(img_skipped, img)
-def test_save_dispose(tmp_path):
+def test_save_dispose(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im_list = [
Image.new("L", (100, 100), "#000"),
@@ -587,10 +611,10 @@ def test_save_dispose(tmp_path):
assert img.disposal_method == i + 1
-def test_dispose2_palette(tmp_path):
+def test_dispose2_palette(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
- # Four colors: white, grey, black, red
+ # Four colors: white, gray, black, red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
im_list = []
@@ -618,7 +642,7 @@ def test_dispose2_palette(tmp_path):
assert rgb_img.getpixel((50, 50)) == circle
-def test_dispose2_diff(tmp_path):
+def test_dispose2_diff(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
# 4 frames: red/blue, red/red, blue/blue, red/blue
@@ -660,7 +684,7 @@ def test_dispose2_diff(tmp_path):
assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0)
-def test_dispose2_background(tmp_path):
+def test_dispose2_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im_list = []
@@ -686,7 +710,7 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0)
-def test_dispose2_background_frame(tmp_path):
+def test_dispose2_background_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im_list = [Image.new("RGBA", (1, 20))]
@@ -704,7 +728,7 @@ def test_dispose2_background_frame(tmp_path):
assert im.n_frames == 3
-def test_transparency_in_second_frame(tmp_path):
+def test_transparency_in_second_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0
@@ -724,7 +748,7 @@ def test_transparency_in_second_frame(tmp_path):
)
-def test_no_transparency_in_second_frame():
+def test_no_transparency_in_second_frame() -> None:
with Image.open("Tests/images/iss634.gif") as img:
# Seek to the second frame
img.seek(img.tell() + 1)
@@ -734,7 +758,7 @@ def test_no_transparency_in_second_frame():
assert img.histogram()[255] == 0
-def test_remapped_transparency(tmp_path):
+def test_remapped_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("P", (1, 2))
@@ -750,7 +774,7 @@ def test_remapped_transparency(tmp_path):
assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
-def test_duration(tmp_path):
+def test_duration(tmp_path: Path) -> None:
duration = 1000
out = str(tmp_path / "temp.gif")
@@ -764,7 +788,7 @@ def test_duration(tmp_path):
assert reread.info["duration"] == duration
-def test_multiple_duration(tmp_path):
+def test_multiple_duration(tmp_path: Path) -> None:
duration_list = [1000, 2000, 3000]
out = str(tmp_path / "temp.gif")
@@ -799,7 +823,7 @@ def test_multiple_duration(tmp_path):
pass
-def test_roundtrip_info_duration(tmp_path):
+def test_roundtrip_info_duration(tmp_path: Path) -> None:
duration_list = [100, 500, 500]
out = str(tmp_path / "temp.gif")
@@ -816,7 +840,7 @@ def test_roundtrip_info_duration(tmp_path):
] == duration_list
-def test_roundtrip_info_duration_combined(tmp_path):
+def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/duplicate_frame.gif") as im:
assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
@@ -832,7 +856,7 @@ def test_roundtrip_info_duration_combined(tmp_path):
] == [1000, 2000]
-def test_identical_frames(tmp_path):
+def test_identical_frames(tmp_path: Path) -> None:
duration_list = [1000, 1500, 2000, 4000]
out = str(tmp_path / "temp.gif")
@@ -856,9 +880,16 @@ def test_identical_frames(tmp_path):
@pytest.mark.parametrize(
- "duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)
+ "duration",
+ (
+ [1000, 1500, 2000],
+ (1000, 1500, 2000),
+ # One more duration than the number of frames
+ [1000, 1500, 2000, 4000],
+ 1500,
+ ),
)
-def test_identical_frames_to_single_frame(duration, tmp_path):
+def test_identical_frames_to_single_frame(duration, tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im_list = [
Image.new("L", (100, 100), "#000"),
@@ -872,10 +903,10 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
assert reread.n_frames == 1
# Assert that the new duration is the total of the identical frames
- assert reread.info["duration"] == 8500
+ assert reread.info["duration"] == 4500
-def test_loop_none(tmp_path):
+def test_loop_none(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("L", (100, 100), "#000")
im.save(out, loop=None)
@@ -883,7 +914,7 @@ def test_loop_none(tmp_path):
assert "loop" not in reread.info
-def test_number_of_loops(tmp_path):
+def test_number_of_loops(tmp_path: Path) -> None:
number_of_loops = 2
out = str(tmp_path / "temp.gif")
@@ -901,7 +932,7 @@ def test_number_of_loops(tmp_path):
assert im.info["loop"] == 2
-def test_background(tmp_path):
+def test_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("L", (100, 100), "#000")
im.info["background"] = 1
@@ -910,7 +941,7 @@ def test_background(tmp_path):
assert reread.info["background"] == im.info["background"]
-def test_webp_background(tmp_path):
+def test_webp_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
# Test opaque WebP background
@@ -925,7 +956,7 @@ def test_webp_background(tmp_path):
im.save(out)
-def test_comment(tmp_path):
+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"
@@ -945,7 +976,7 @@ def test_comment(tmp_path):
assert reread.info["version"] == b"GIF89a"
-def test_comment_over_255(tmp_path):
+def test_comment_over_255(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("L", (100, 100), "#000")
comment = b"Test comment text"
@@ -960,18 +991,18 @@ def test_comment_over_255(tmp_path):
assert reread.info["version"] == b"GIF89a"
-def test_zero_comment_subblocks():
+def test_zero_comment_subblocks() -> None:
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
assert_image_equal_tofile(im, TEST_GIF)
-def test_read_multiple_comment_blocks():
+def test_read_multiple_comment_blocks() -> None:
with Image.open("Tests/images/multiple_comments.gif") as im:
# Multiple comment blocks in a frame are separated not concatenated
assert im.info["comment"] == b"Test comment 1\nTest comment 2"
-def test_empty_string_comment(tmp_path):
+def test_empty_string_comment(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/chi.gif") as im:
assert "comment" in im.info
@@ -984,7 +1015,7 @@ def test_empty_string_comment(tmp_path):
assert "comment" not in frame.info
-def test_retain_comment_in_subsequent_frames(tmp_path):
+def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
# Test that a comment block at the beginning is kept
with Image.open("Tests/images/chi.gif") as im:
for frame in ImageSequence.Iterator(im):
@@ -1015,10 +1046,10 @@ def test_retain_comment_in_subsequent_frames(tmp_path):
assert frame.info["comment"] == b"Test"
-def test_version(tmp_path):
+def test_version(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
- def assert_version_after_save(im, version):
+ def assert_version_after_save(im, version) -> None:
im.save(out)
with Image.open(out) as reread:
assert reread.info["version"] == version
@@ -1045,7 +1076,7 @@ def test_version(tmp_path):
assert_version_after_save(im, b"GIF87a")
-def test_append_images(tmp_path):
+def test_append_images(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
# Test appending single frame images
@@ -1074,7 +1105,7 @@ def test_append_images(tmp_path):
assert reread.n_frames == 10
-def test_transparent_optimize(tmp_path):
+def test_transparent_optimize(tmp_path: Path) -> None:
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
# transparency.
# Need a palette that isn't using the 0 color,
@@ -1094,7 +1125,7 @@ def test_transparent_optimize(tmp_path):
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
-def test_removed_transparency(tmp_path):
+def test_removed_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("RGB", (256, 1))
@@ -1109,7 +1140,7 @@ def test_removed_transparency(tmp_path):
assert "transparency" not in reloaded.info
-def test_rgb_transparency(tmp_path):
+def test_rgb_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
# Single frame
@@ -1131,7 +1162,7 @@ def test_rgb_transparency(tmp_path):
assert "transparency" not in reloaded.info
-def test_rgba_transparency(tmp_path):
+def test_rgba_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = hopper("P")
@@ -1142,7 +1173,13 @@ def test_rgba_transparency(tmp_path):
assert_image_equal(hopper("P").convert("RGB"), reloaded)
-def test_bbox(tmp_path):
+def test_background_outside_palettte(tmp_path: Path) -> 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")
im = Image.new("RGB", (100, 100), "#fff")
@@ -1153,7 +1190,7 @@ def test_bbox(tmp_path):
assert reread.n_frames == 2
-def test_bbox_alpha(tmp_path):
+def test_bbox_alpha(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("RGBA", (1, 2), (255, 0, 0, 255))
@@ -1165,7 +1202,7 @@ def test_bbox_alpha(tmp_path):
assert reread.n_frames == 2
-def test_palette_save_L(tmp_path):
+def test_palette_save_L(tmp_path: Path) -> None:
# Generate an L mode image with a separate palette
im = hopper("P")
@@ -1179,7 +1216,7 @@ def test_palette_save_L(tmp_path):
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
-def test_palette_save_P(tmp_path):
+def test_palette_save_P(tmp_path: Path) -> None:
im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1)
@@ -1193,7 +1230,7 @@ def test_palette_save_P(tmp_path):
assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
-def test_palette_save_duplicate_entries(tmp_path):
+def test_palette_save_duplicate_entries(tmp_path: Path) -> None:
im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1)
@@ -1206,7 +1243,7 @@ def test_palette_save_duplicate_entries(tmp_path):
assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0)
-def test_palette_save_all_P(tmp_path):
+def test_palette_save_all_P(tmp_path: Path) -> None:
frames = []
colors = ((255, 0, 0), (0, 255, 0))
for color in colors:
@@ -1229,7 +1266,7 @@ def test_palette_save_all_P(tmp_path):
assert im.palette.palette == im.global_palette.palette
-def test_palette_save_ImagePalette(tmp_path):
+def test_palette_save_ImagePalette(tmp_path: Path) -> None:
# Pass in a different palette, as an ImagePalette.ImagePalette
# effectively the same as test_palette_save_P
@@ -1244,7 +1281,7 @@ def test_palette_save_ImagePalette(tmp_path):
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
-def test_save_I(tmp_path):
+def test_save_I(tmp_path: Path) -> None:
# Test saving something that would trigger the auto-convert to 'L'
im = hopper("I")
@@ -1256,7 +1293,7 @@ def test_save_I(tmp_path):
assert_image_equal(reloaded.convert("L"), im.convert("L"))
-def test_getdata():
+def test_getdata() -> None:
# Test getheader/getdata against legacy values.
# Create a 'P' image with holes in the palette.
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
@@ -1284,7 +1321,7 @@ def test_getdata():
GifImagePlugin._FORCE_OPTIMIZE = False
-def test_lzw_bits():
+def test_lzw_bits() -> None:
# see https://github.com/python-pillow/Pillow/issues/2811
with Image.open("Tests/images/issue_2811.gif") as im:
assert im.tile[0][3][0] == 11 # LZW bits
@@ -1292,7 +1329,7 @@ def test_lzw_bits():
im.load()
-def test_extents():
+def test_extents() -> None:
with Image.open("Tests/images/test_extents.gif") as im:
assert im.size == (100, 100)
@@ -1304,7 +1341,7 @@ def test_extents():
assert im.size == (150, 150)
-def test_missing_background():
+def test_missing_background() -> None:
# The Global Color Table Flag isn't set, so there is no background color index,
# but the disposal method is "Restore to background color"
with Image.open("Tests/images/missing_background.gif") as im:
@@ -1312,7 +1349,7 @@ def test_missing_background():
assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png")
-def test_saving_rgba(tmp_path):
+def test_saving_rgba(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/transparent.png") as im:
im.save(out)
diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py
index 3f056fdae..006ee952d 100644
--- a/Tests/test_file_gimpgradient.py
+++ b/Tests/test_file_gimpgradient.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from PIL import GimpGradientFile, ImagePalette
-def test_linear_pos_le_middle():
+def test_linear_pos_le_middle() -> None:
# Arrange
middle = 0.5
pos = 0.25
@@ -13,7 +15,7 @@ def test_linear_pos_le_middle():
assert ret == 0.25
-def test_linear_pos_le_small_middle():
+def test_linear_pos_le_small_middle() -> None:
# Arrange
middle = 1e-11
pos = 1e-12
@@ -25,7 +27,7 @@ def test_linear_pos_le_small_middle():
assert ret == 0.0
-def test_linear_pos_gt_middle():
+def test_linear_pos_gt_middle() -> None:
# Arrange
middle = 0.5
pos = 0.75
@@ -37,7 +39,7 @@ def test_linear_pos_gt_middle():
assert ret == 0.75
-def test_linear_pos_gt_small_middle():
+def test_linear_pos_gt_small_middle() -> None:
# Arrange
middle = 1 - 1e-11
pos = 1 - 1e-12
@@ -49,7 +51,7 @@ def test_linear_pos_gt_small_middle():
assert ret == 1.0
-def test_curved():
+def test_curved() -> None:
# Arrange
middle = 0.5
pos = 0.75
@@ -61,7 +63,7 @@ def test_curved():
assert ret == 0.75
-def test_sine():
+def test_sine() -> None:
# Arrange
middle = 0.5
pos = 0.75
@@ -73,7 +75,7 @@ def test_sine():
assert ret == 0.8535533905932737
-def test_sphere_increasing():
+def test_sphere_increasing() -> None:
# Arrange
middle = 0.5
pos = 0.75
@@ -85,7 +87,7 @@ def test_sphere_increasing():
assert round(abs(ret - 0.9682458365518543), 7) == 0
-def test_sphere_decreasing():
+def test_sphere_decreasing() -> None:
# Arrange
middle = 0.5
pos = 0.75
@@ -97,7 +99,7 @@ def test_sphere_decreasing():
assert ret == 0.3385621722338523
-def test_load_via_imagepalette():
+def test_load_via_imagepalette() -> None:
# Arrange
test_file = "Tests/images/gimp_gradient.ggr"
@@ -110,7 +112,7 @@ def test_load_via_imagepalette():
assert palette[1] == "RGBA"
-def test_load_1_3_via_imagepalette():
+def test_load_1_3_via_imagepalette() -> None:
# Arrange
# GIMP 1.3 gradient files contain a name field
test_file = "Tests/images/gimp_gradient_with_name.ggr"
diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py
index caec9cf21..e8d5f1705 100644
--- a/Tests/test_file_gimppalette.py
+++ b/Tests/test_file_gimppalette.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
import pytest
from PIL.GimpPaletteFile import GimpPaletteFile
-def test_sanity():
+def test_sanity() -> None:
with open("Tests/images/test.gpl", "rb") as fp:
GimpPaletteFile(fp)
@@ -20,7 +22,7 @@ def test_sanity():
GimpPaletteFile(fp)
-def test_get_palette():
+def test_get_palette() -> None:
# Arrange
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
palette_file = GimpPaletteFile(fp)
diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py
index dd1c5e7d2..4945468be 100644
--- a/Tests/test_file_gribstub.py
+++ b/Tests/test_file_gribstub.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import GribStubImagePlugin, Image
@@ -7,7 +11,7 @@ from .helper import hopper
TEST_FILE = "Tests/images/WAlaska.wind.7days.grb"
-def test_open():
+def test_open() -> None:
# Act
with Image.open(TEST_FILE) as im:
# Assert
@@ -18,7 +22,7 @@ def test_open():
assert im.size == (1, 1)
-def test_invalid_file():
+def test_invalid_file() -> None:
# Arrange
invalid_file = "Tests/images/flower.jpg"
@@ -27,7 +31,7 @@ def test_invalid_file():
GribStubImagePlugin.GribStubImageFile(invalid_file)
-def test_load():
+def test_load() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler
@@ -35,7 +39,7 @@ def test_load():
im.load()
-def test_save(tmp_path):
+def test_save(tmp_path: Path) -> None:
# Arrange
im = hopper()
tmpfile = str(tmp_path / "temp.grib")
@@ -45,13 +49,13 @@ def test_save(tmp_path):
im.save(tmpfile)
-def test_handler(tmp_path):
+def test_handler(tmp_path: Path) -> None:
class TestHandler:
opened = False
loaded = False
saved = False
- def open(self, im):
+ def open(self, im) -> None:
self.opened = True
def load(self, im):
@@ -59,7 +63,7 @@ def test_handler(tmp_path):
im.fp.close()
return Image.new("RGB", (1, 1))
- def save(self, im, fp, filename):
+ def save(self, im, fp, filename) -> None:
self.saved = True
handler = TestHandler()
diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py
index 7ca10fac5..ac3d40bf2 100644
--- a/Tests/test_file_hdf5stub.py
+++ b/Tests/test_file_hdf5stub.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Hdf5StubImagePlugin, Image
@@ -5,7 +9,7 @@ from PIL import Hdf5StubImagePlugin, Image
TEST_FILE = "Tests/images/hdf5.h5"
-def test_open():
+def test_open() -> None:
# Act
with Image.open(TEST_FILE) as im:
# Assert
@@ -16,7 +20,7 @@ def test_open():
assert im.size == (1, 1)
-def test_invalid_file():
+def test_invalid_file() -> None:
# Arrange
invalid_file = "Tests/images/flower.jpg"
@@ -25,7 +29,7 @@ def test_invalid_file():
Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file)
-def test_load():
+def test_load() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler
@@ -33,7 +37,7 @@ def test_load():
im.load()
-def test_save():
+def test_save() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
dummy_fp = None
@@ -46,13 +50,13 @@ def test_save():
Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename)
-def test_handler(tmp_path):
+def test_handler(tmp_path: Path) -> None:
class TestHandler:
opened = False
loaded = False
saved = False
- def open(self, im):
+ def open(self, im) -> None:
self.opened = True
def load(self, im):
@@ -60,7 +64,7 @@ def test_handler(tmp_path):
im.fp.close()
return Image.new("RGB", (1, 1))
- def save(self, im, fp, filename):
+ def save(self, im, fp, filename) -> None:
self.saved = True
handler = TestHandler()
diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py
index 42275424d..488984aef 100644
--- a/Tests/test_file_icns.py
+++ b/Tests/test_file_icns.py
@@ -1,6 +1,9 @@
+from __future__ import annotations
+
import io
import os
import warnings
+from pathlib import Path
import pytest
@@ -12,7 +15,7 @@ from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless
TEST_FILE = "Tests/images/pillow.icns"
-def test_sanity():
+def test_sanity() -> None:
# Loading this icon by default should result in the largest size
# (512x512@2x) being loaded
with Image.open(TEST_FILE) as im:
@@ -25,7 +28,7 @@ def test_sanity():
assert im.format == "ICNS"
-def test_load():
+def test_load() -> None:
with Image.open(TEST_FILE) as im:
assert im.load()[0, 0] == (0, 0, 0, 0)
@@ -33,7 +36,7 @@ def test_load():
assert im.load()[0, 0] == (0, 0, 0, 0)
-def test_save(tmp_path):
+def test_save(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.icns")
with Image.open(TEST_FILE) as im:
@@ -50,7 +53,7 @@ def test_save(tmp_path):
assert _binary.i32be(fp.read(4)) == file_length
-def test_save_append_images(tmp_path):
+def test_save_append_images(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.icns")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128))
@@ -65,7 +68,7 @@ def test_save_append_images(tmp_path):
assert_image_equal(reread, provided_im)
-def test_save_fp():
+def test_save_fp() -> None:
fp = io.BytesIO()
with Image.open(TEST_FILE) as im:
@@ -77,7 +80,7 @@ def test_save_fp():
assert reread.format == "ICNS"
-def test_sizes():
+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:
@@ -94,7 +97,7 @@ def test_sizes():
im.size = (1, 1)
-def test_older_icon():
+def test_older_icon() -> None:
# This icon was made with Icon Composer rather than iconutil; it still
# uses PNG rather than JP2, however (since it was made on 10.9).
with Image.open("Tests/images/pillow2.icns") as im:
@@ -109,7 +112,7 @@ def test_older_icon():
@skip_unless_feature("jpg_2000")
-def test_jp2_icon():
+def test_jp2_icon() -> None:
# This icon uses JPEG 2000 images instead of the PNG images.
# The advantage of doing this is that OS X 10.5 supports JPEG 2000
# but not PNG; some commercial software therefore does just this.
@@ -125,7 +128,7 @@ def test_jp2_icon():
assert im2.size == (wr, hr)
-def test_getimage():
+def test_getimage() -> None:
with open(TEST_FILE, "rb") as fp:
icns_file = IcnsImagePlugin.IcnsFile(fp)
@@ -138,14 +141,14 @@ def test_getimage():
assert im.size == (512, 512)
-def test_not_an_icns_file():
+def test_not_an_icns_file() -> None:
with io.BytesIO(b"invalid\n") as fp:
with pytest.raises(SyntaxError):
IcnsImagePlugin.IcnsFile(fp)
@skip_unless_feature("jpg_2000")
-def test_icns_decompression_bomb():
+def test_icns_decompression_bomb() -> None:
with Image.open(
"Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns"
) as im:
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index 4e6dbe6ed..65f090931 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
import io
import os
+from pathlib import Path
import pytest
@@ -10,7 +13,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper
TEST_ICO_FILE = "Tests/images/hopper.ico"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(TEST_ICO_FILE) as im:
im.load()
assert im.mode == "RGBA"
@@ -19,29 +22,29 @@ def test_sanity():
assert im.get_format_mimetype() == "image/x-icon"
-def test_load():
+def test_load() -> None:
with Image.open(TEST_ICO_FILE) as im:
assert im.load()[0, 0] == (1, 1, 9, 255)
-def test_mask():
+def test_mask() -> None:
with Image.open("Tests/images/hopper_mask.ico") as im:
assert_image_equal_tofile(im, "Tests/images/hopper_mask.png")
-def test_black_and_white():
+def test_black_and_white() -> None:
with Image.open("Tests/images/black_and_white.ico") as im:
assert im.mode == "RGBA"
assert im.size == (16, 16)
-def test_invalid_file():
+def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):
IcoImagePlugin.IcoImageFile(fp)
-def test_save_to_bytes():
+def test_save_to_bytes() -> None:
output = io.BytesIO()
im = hopper()
im.save(output, "ico", sizes=[(32, 32), (64, 64)])
@@ -71,7 +74,7 @@ def test_save_to_bytes():
)
-def test_getpixel(tmp_path):
+def test_getpixel(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico")
im = hopper()
@@ -84,7 +87,7 @@ def test_getpixel(tmp_path):
assert reloaded.getpixel((0, 0)) == (18, 20, 62)
-def test_no_duplicates(tmp_path):
+def test_no_duplicates(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
@@ -98,7 +101,7 @@ def test_no_duplicates(tmp_path):
assert os.path.getsize(temp_file) == os.path.getsize(temp_file2)
-def test_different_bit_depths(tmp_path):
+def test_different_bit_depths(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
@@ -132,7 +135,7 @@ def test_different_bit_depths(tmp_path):
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
-def test_save_to_bytes_bmp(mode):
+def test_save_to_bytes_bmp(mode) -> None:
output = io.BytesIO()
im = hopper(mode)
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
@@ -160,13 +163,13 @@ def test_save_to_bytes_bmp(mode):
assert_image_equal(reloaded, im)
-def test_incorrect_size():
+def test_incorrect_size() -> None:
with Image.open(TEST_ICO_FILE) as im:
with pytest.raises(ValueError):
im.size = (1, 1)
-def test_save_256x256(tmp_path):
+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:
@@ -179,7 +182,7 @@ def test_save_256x256(tmp_path):
assert im_saved.size == (256, 256)
-def test_only_save_relevant_sizes(tmp_path):
+def test_only_save_relevant_sizes(tmp_path: Path) -> None:
"""Issue #2266 https://github.com/python-pillow/Pillow/issues/2266
Should save in 16x16, 24x24, 32x32, 48x48 sizes
and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes
@@ -195,7 +198,7 @@ def test_only_save_relevant_sizes(tmp_path):
assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)}
-def test_save_append_images(tmp_path):
+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))
@@ -209,7 +212,7 @@ def test_save_append_images(tmp_path):
assert_image_equal(reread, provided_im)
-def test_unexpected_size():
+def test_unexpected_size() -> None:
# This image has been manually hexedited to state that it is 16x32
# while the image within is still 16x16
with pytest.warns(UserWarning):
@@ -217,7 +220,7 @@ def test_unexpected_size():
assert im.size == (16, 16)
-def test_draw_reloaded(tmp_path):
+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")
diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py
index fd00f260e..f932069b9 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
import filecmp
import warnings
+from pathlib import Path
import pytest
@@ -11,7 +14,7 @@ from .helper import assert_image_equal_tofile, hopper, is_pypy
TEST_IM = "Tests/images/hopper.im"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(TEST_IM) as im:
im.load()
assert im.mode == "RGB"
@@ -19,7 +22,7 @@ def test_sanity():
assert im.format == "IM"
-def test_name_limit(tmp_path):
+def test_name_limit(tmp_path: Path) -> None:
out = str(tmp_path / ("name_limit_test" * 7 + ".im"))
with Image.open(TEST_IM) as im:
im.save(out)
@@ -27,8 +30,8 @@ def test_name_limit(tmp_path):
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
-def test_unclosed_file():
- def open():
+def test_unclosed_file() -> None:
+ def open() -> None:
im = Image.open(TEST_IM)
im.load()
@@ -36,20 +39,20 @@ def test_unclosed_file():
open()
-def test_closed_file():
+def test_closed_file() -> None:
with warnings.catch_warnings():
im = Image.open(TEST_IM)
im.load()
im.close()
-def test_context_manager():
+def test_context_manager() -> None:
with warnings.catch_warnings():
with Image.open(TEST_IM) as im:
im.load()
-def test_tell():
+def test_tell() -> None:
# Arrange
with Image.open(TEST_IM) as im:
# Act
@@ -59,13 +62,13 @@ def test_tell():
assert frame == 0
-def test_n_frames():
+def test_n_frames() -> None:
with Image.open(TEST_IM) as im:
assert im.n_frames == 1
assert not im.is_animated
-def test_eoferror():
+def test_eoferror() -> None:
with Image.open(TEST_IM) as im:
n_frames = im.n_frames
@@ -79,14 +82,14 @@ def test_eoferror():
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
-def test_roundtrip(mode, tmp_path):
+def test_roundtrip(mode, tmp_path: Path) -> None:
out = str(tmp_path / "temp.im")
im = hopper(mode)
im.save(out)
assert_image_equal_tofile(im, out)
-def test_small_palette(tmp_path):
+def test_small_palette(tmp_path: Path) -> None:
im = Image.new("P", (1, 1))
colors = [0, 1, 2]
im.putpalette(colors)
@@ -98,19 +101,19 @@ def test_small_palette(tmp_path):
assert reloaded.getpalette() == colors + [0] * 765
-def test_save_unsupported_mode(tmp_path):
+def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.im")
im = hopper("HSV")
with pytest.raises(ValueError):
im.save(out)
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
ImImagePlugin.ImImageFile(invalid_file)
-def test_number():
+def test_number() -> None:
assert ImImagePlugin.number("1.2") == 1.2
diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py
index f56acc429..6957dfa0a 100644
--- a/Tests/test_file_imt.py
+++ b/Tests/test_file_imt.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import io
import pytest
@@ -7,13 +9,13 @@ from PIL import Image, ImtImagePlugin
from .helper import assert_image_equal_tofile
-def test_sanity():
+def test_sanity() -> None:
with Image.open("Tests/images/bw_gradient.imt") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n"))
-def test_invalid_file(data):
+def test_invalid_file(data: bytes) -> None:
with io.BytesIO(data) as fp:
with pytest.raises(SyntaxError):
ImtImagePlugin.ImtImageFile(fp)
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index d2edcfc27..9c0969437 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -1,14 +1,30 @@
+from __future__ import annotations
+
import sys
from io import BytesIO, StringIO
+import pytest
+
from PIL import Image, IptcImagePlugin
-from .helper import hopper
+from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/iptc.jpg"
-def test_getiptcinfo_jpg_none():
+def test_open() -> None:
+ expected = Image.new("L", (1, 1))
+
+ f = BytesIO(
+ b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
+ b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
+ )
+ with Image.open(f) as im:
+ assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
+ assert_image_equal(im, expected)
+
+
+def test_getiptcinfo_jpg_none() -> None:
# Arrange
with hopper() as im:
# Act
@@ -18,7 +34,7 @@ def test_getiptcinfo_jpg_none():
assert iptc is None
-def test_getiptcinfo_jpg_found():
+def test_getiptcinfo_jpg_found() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
# Act
@@ -30,7 +46,7 @@ def test_getiptcinfo_jpg_found():
assert iptc[(2, 101)] == b"Hungary"
-def test_getiptcinfo_fotostation():
+def test_getiptcinfo_fotostation() -> None:
# Arrange
with open(TEST_FILE, "rb") as fp:
data = bytearray(fp.read())
@@ -44,10 +60,10 @@ def test_getiptcinfo_fotostation():
for tag in iptc.keys():
if tag[0] == 240:
return
- assert False, "FotoStation tag not found"
+ pytest.fail("FotoStation tag not found")
-def test_getiptcinfo_zero_padding():
+def test_getiptcinfo_zero_padding() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
im.info["photoshop"][0x0404] += b"\x00\x00\x00"
@@ -60,7 +76,7 @@ def test_getiptcinfo_zero_padding():
assert len(iptc) == 3
-def test_getiptcinfo_tiff_none():
+def test_getiptcinfo_tiff_none() -> None:
# Arrange
with Image.open("Tests/images/hopper.tif") as im:
# Act
@@ -70,29 +86,33 @@ def test_getiptcinfo_tiff_none():
assert iptc is None
-def test_i():
+def test_i() -> None:
# Arrange
c = b"a"
# Act
- ret = IptcImagePlugin.i(c)
+ with pytest.warns(DeprecationWarning):
+ ret = IptcImagePlugin.i(c)
# Assert
assert ret == 97
-def test_dump():
+def test_dump(monkeypatch) -> None:
# Arrange
c = b"abc"
# Temporarily redirect stdout
- old_stdout = sys.stdout
- sys.stdout = mystdout = StringIO()
+ mystdout = StringIO()
+ monkeypatch.setattr(sys, "stdout", mystdout)
# Act
- IptcImagePlugin.dump(c)
-
- # Reset stdout
- sys.stdout = old_stdout
+ with pytest.warns(DeprecationWarning):
+ IptcImagePlugin.dump(c)
# Assert
assert mystdout.getvalue() == "61 62 63 \n"
+
+
+def test_pad_deprecation() -> None:
+ with pytest.warns(DeprecationWarning):
+ assert IptcImagePlugin.PAD == b"\0\0\0\0"
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index a0822d000..ff278d4c1 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -1,7 +1,10 @@
+from __future__ import annotations
+
import os
import re
import warnings
from io import BytesIO
+from pathlib import Path
import pytest
@@ -48,7 +51,7 @@ class TestFileJpeg:
im.bytes = test_bytes # for testing only
return im
- def gen_random_image(self, size, mode="RGB"):
+ def gen_random_image(self, size, mode: str = "RGB"):
"""Generates a very hard to compress file
:param size: tuple
:param mode: optional image mode
@@ -56,7 +59,7 @@ class TestFileJpeg:
"""
return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode)))
- def test_sanity(self):
+ def test_sanity(self) -> None:
# internal version number
assert re.search(r"\d+\.\d+$", features.version_codec("jpg"))
@@ -68,13 +71,13 @@ class TestFileJpeg:
assert im.get_format_mimetype() == "image/jpeg"
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
- def test_zero(self, size, tmp_path):
+ def test_zero(self, size, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
im = Image.new("RGB", size)
with pytest.raises(ValueError):
im.save(f)
- def test_app(self):
+ def test_app(self) -> None:
# Test APP/COM reader (@PIL135)
with Image.open(TEST_FILE) as im:
assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00")
@@ -87,7 +90,7 @@ class TestFileJpeg:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
assert im.app["COM"] == im.info["comment"]
- def test_comment_write(self):
+ def test_comment_write(self) -> None:
with Image.open(TEST_FILE) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
@@ -113,7 +116,7 @@ class TestFileJpeg:
comment = comment.encode()
assert reloaded.info["comment"] == comment
- def test_cmyk(self):
+ def test_cmyk(self) -> None:
# Test CMYK handling. Thanks to Tim and Charlie for test data,
# Michael for getting me to look one more time.
f = "Tests/images/pil_sample_cmyk.jpg"
@@ -141,11 +144,24 @@ class TestFileJpeg:
)
assert k > 0.9
+ def test_rgb(self) -> None:
+ def getchannels(im):
+ return tuple(v[0] for v in im.layer)
+
+ im = hopper()
+ im_ycbcr = self.roundtrip(im)
+ assert getchannels(im_ycbcr) == (1, 2, 3)
+ assert_image_similar(im, im_ycbcr, 17)
+
+ im_rgb = self.roundtrip(im, keep_rgb=True)
+ assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B"))
+ assert_image_similar(im, im_rgb, 12)
+
@pytest.mark.parametrize(
"test_image_path",
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
)
- def test_dpi(self, test_image_path):
+ def test_dpi(self, test_image_path) -> None:
def test(xdpi, ydpi=None):
with Image.open(test_image_path) as im:
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
@@ -159,7 +175,7 @@ class TestFileJpeg:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_icc(self, tmp_path):
+ def test_icc(self, tmp_path: Path) -> None:
# Test ICC support
with Image.open("Tests/images/rgb.jpg") as im1:
icc_profile = im1.info["icc_profile"]
@@ -191,7 +207,7 @@ class TestFileJpeg:
ImageFile.MAXBLOCK * 4 + 3, # large block
),
)
- def test_icc_big(self, n):
+ def test_icc_big(self, n) -> None:
# Make sure that the "extra" support handles large blocks
# The ICC APP marker can store 65519 bytes per marker, so
# using a 4-byte test code should allow us to detect out of
@@ -204,7 +220,7 @@ class TestFileJpeg:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_large_icc_meta(self, tmp_path):
+ def test_large_icc_meta(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148
# Sometimes the meta data on the icc_profile block is bigger than
# Image.MAXBLOCK or the image size.
@@ -228,7 +244,7 @@ class TestFileJpeg:
f = str(tmp_path / "temp3.jpg")
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
- def test_optimize(self):
+ def test_optimize(self) -> None:
im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), optimize=0)
im3 = self.roundtrip(hopper(), optimize=1)
@@ -237,14 +253,14 @@ class TestFileJpeg:
assert im1.bytes >= im2.bytes
assert im1.bytes >= im3.bytes
- def test_optimize_large_buffer(self, tmp_path):
+ def test_optimize_large_buffer(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg")
# this requires ~ 1.5x Image.MAXBLOCK
im = Image.new("RGB", (4096, 4096), 0xFF3333)
im.save(f, format="JPEG", optimize=True)
- def test_progressive(self):
+ def test_progressive(self) -> None:
im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), progressive=False)
im3 = self.roundtrip(hopper(), progressive=True)
@@ -255,25 +271,25 @@ class TestFileJpeg:
assert_image_equal(im1, im3)
assert im1.bytes >= im3.bytes
- def test_progressive_large_buffer(self, tmp_path):
+ def test_progressive_large_buffer(self, tmp_path: Path) -> None:
f = str(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):
+ def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None:
f = str(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)
- def test_progressive_cmyk_buffer(self):
+ def test_progressive_cmyk_buffer(self) -> None:
# Issue 2272, quality 90 cmyk image is tripping the large buffer bug.
f = BytesIO()
im = self.gen_random_image((256, 256), "CMYK")
im.save(f, format="JPEG", progressive=True, quality=94)
- def test_large_exif(self, tmp_path):
+ def test_large_exif(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg")
im = hopper()
@@ -282,12 +298,12 @@ class TestFileJpeg:
with pytest.raises(ValueError):
im.save(f, "JPEG", quality=90, exif=b"1" * 65534)
- def test_exif_typeerror(self):
+ def test_exif_typeerror(self) -> None:
with Image.open("Tests/images/exif_typeerror.jpg") as im:
# Should not raise a TypeError
im._getexif()
- def test_exif_gps(self, tmp_path):
+ def test_exif_gps(self, tmp_path: Path) -> None:
expected_exif_gps = {
0: b"\x00\x00\x00\x01",
2: 4294967295,
@@ -312,7 +328,7 @@ class TestFileJpeg:
exif = reloaded._getexif()
assert exif[gps_index] == expected_exif_gps
- def test_empty_exif_gps(self):
+ def test_empty_exif_gps(self) -> None:
with Image.open("Tests/images/empty_gps_ifd.jpg") as im:
exif = im.getexif()
del exif[0x8769]
@@ -330,7 +346,7 @@ class TestFileJpeg:
# Assert that it was transposed
assert 0x0112 not in exif
- def test_exif_equality(self):
+ def test_exif_equality(self) -> None:
# In 7.2.0, Exif rationals were changed to be read as
# TiffImagePlugin.IFDRational. This class had a bug in __eq__,
# breaking the self-equality of Exif data
@@ -340,7 +356,7 @@ class TestFileJpeg:
exifs.append(im._getexif())
assert exifs[0] == exifs[1]
- def test_exif_rollback(self):
+ def test_exif_rollback(self) -> None:
# rolling back exif support in 3.1 to pre-3.0 formatting.
# expected from 2.9, with b/u qualifiers switched for 3.2 compatibility
# this test passes on 2.9 and 3.1, but not 3.0
@@ -375,12 +391,12 @@ class TestFileJpeg:
for tag, value in expected_exif.items():
assert value == exif[tag]
- def test_exif_gps_typeerror(self):
+ def test_exif_gps_typeerror(self) -> None:
with Image.open("Tests/images/exif_gps_typeerror.jpg") as im:
# Should not raise a TypeError
im._getexif()
- def test_progressive_compat(self):
+ def test_progressive_compat(self) -> None:
im1 = self.roundtrip(hopper())
assert not im1.info.get("progressive")
assert not im1.info.get("progression")
@@ -401,7 +417,7 @@ class TestFileJpeg:
assert im3.info.get("progressive")
assert im3.info.get("progression")
- def test_quality(self):
+ def test_quality(self) -> None:
im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), quality=50)
assert_image(im1, im2.mode, im2.size)
@@ -411,57 +427,60 @@ class TestFileJpeg:
assert_image(im1, im3.mode, im3.size)
assert im2.bytes > im3.bytes
- def test_smooth(self):
+ def test_smooth(self) -> None:
im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), smooth=100)
assert_image(im1, im2.mode, im2.size)
- def test_subsampling(self):
+ def test_subsampling(self) -> None:
def getsampling(im):
layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
# experimental API
- im = self.roundtrip(hopper(), subsampling=-1) # default
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling=0) # 4:4:4
- assert getsampling(im) == (1, 1, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling=1) # 4:2:2
- assert getsampling(im) == (2, 1, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling=2) # 4:2:0
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling=3) # default (undefined)
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
+ for subsampling in (-1, 3): # (default, invalid)
+ im = self.roundtrip(hopper(), subsampling=subsampling)
+ assert getsampling(im) == (2, 2, 1, 1, 1, 1)
+ for subsampling in (0, "4:4:4"):
+ im = self.roundtrip(hopper(), subsampling=subsampling)
+ assert getsampling(im) == (1, 1, 1, 1, 1, 1)
+ for subsampling in (1, "4:2:2"):
+ im = self.roundtrip(hopper(), subsampling=subsampling)
+ assert getsampling(im) == (2, 1, 1, 1, 1, 1)
+ for subsampling in (2, "4:2:0", "4:1:1"):
+ im = self.roundtrip(hopper(), subsampling=subsampling)
+ assert getsampling(im) == (2, 2, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling="4:4:4")
- assert getsampling(im) == (1, 1, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling="4:2:2")
- assert getsampling(im) == (2, 1, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling="4:2:0")
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling="4:1:1")
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
+ # RGB colorspace
+ for subsampling in (-1, 0, "4:4:4"):
+ # "4:4:4" doesn't really make sense for RGB, but the conversion
+ # to an integer happens at a higher level
+ im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
+ assert getsampling(im) == (1, 1, 1, 1, 1, 1)
+ for subsampling in (1, "4:2:2", 2, "4:2:0", 3):
+ with pytest.raises(OSError):
+ self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
with pytest.raises(TypeError):
self.roundtrip(hopper(), subsampling="1:1:1")
- def test_exif(self):
+ def test_exif(self) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
info = im._getexif()
assert info[305] == "Adobe Photoshop CS Macintosh"
- def test_get_child_images(self):
+ def test_get_child_images(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
ims = im.get_child_images()
assert len(ims) == 1
assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1)
- def test_mp(self):
+ def test_mp(self) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im._getmp() is None
- def test_quality_keep(self, tmp_path):
+ 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")
@@ -475,13 +494,13 @@ class TestFileJpeg:
f = str(tmp_path / "temp.jpg")
im.save(f, quality="keep")
- def test_junk_jpeg_header(self):
+ def test_junk_jpeg_header(self) -> None:
# https://github.com/python-pillow/Pillow/issues/630
filename = "Tests/images/junk_jpeg_header.jpg"
with Image.open(filename):
pass
- def test_ff00_jpeg_header(self):
+ def test_ff00_jpeg_header(self) -> None:
filename = "Tests/images/jpeg_ff00_header.jpg"
with Image.open(filename):
pass
@@ -489,7 +508,7 @@ class TestFileJpeg:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_truncated_jpeg_should_read_all_the_data(self):
+ def test_truncated_jpeg_should_read_all_the_data(self) -> None:
filename = "Tests/images/truncated_jpeg.jpg"
ImageFile.LOAD_TRUNCATED_IMAGES = True
with Image.open(filename) as im:
@@ -497,7 +516,7 @@ class TestFileJpeg:
ImageFile.LOAD_TRUNCATED_IMAGES = False
assert im.getbbox() is not None
- def test_truncated_jpeg_throws_oserror(self):
+ def test_truncated_jpeg_throws_oserror(self) -> None:
filename = "Tests/images/truncated_jpeg.jpg"
with Image.open(filename) as im:
with pytest.raises(OSError):
@@ -510,8 +529,8 @@ class TestFileJpeg:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_qtables(self, tmp_path):
- def _n_qtables_helper(n, test_file):
+ def test_qtables(self, tmp_path: Path) -> None:
+ def _n_qtables_helper(n, test_file) -> None:
with Image.open(test_file) as im:
f = str(tmp_path / "temp.jpg")
im.save(f, qtables=[[n] * 64] * n)
@@ -619,45 +638,62 @@ class TestFileJpeg:
with pytest.raises(ValueError):
self.roundtrip(im, qtables=[[1, 2, 3, 4]])
- def test_load_16bit_qtables(self):
+ def test_load_16bit_qtables(self) -> None:
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
assert len(im.quantization) == 2
assert len(im.quantization[0]) == 64
assert max(im.quantization[0]) > 255
- def test_save_multiple_16bit_qtables(self):
+ def test_save_multiple_16bit_qtables(self) -> None:
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
im2 = self.roundtrip(im, qtables="keep")
assert im.quantization == im2.quantization
- def test_save_single_16bit_qtable(self):
+ def test_save_single_16bit_qtable(self) -> None:
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
im2 = self.roundtrip(im, qtables={0: im.quantization[0]})
assert len(im2.quantization) == 1
assert im2.quantization[0] == im.quantization[0]
- def test_save_low_quality_baseline_qtables(self):
+ def test_save_low_quality_baseline_qtables(self) -> None:
with Image.open(TEST_FILE) as im:
im2 = self.roundtrip(im, quality=10)
assert len(im2.quantization) == 2
assert max(im2.quantization[0]) <= 255
assert max(im2.quantization[1]) <= 255
+ @pytest.mark.parametrize(
+ "blocks, rows, markers",
+ ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)),
+ )
+ def test_restart_markers(self, blocks, rows, markers) -> None:
+ im = Image.new("RGB", (32, 32)) # 16 MCUs
+ out = BytesIO()
+ im.save(
+ out,
+ format="JPEG",
+ restart_marker_blocks=blocks,
+ restart_marker_rows=rows,
+ # force 8x8 pixel MCUs
+ subsampling=0,
+ )
+ assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers
+
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
- def test_load_djpeg(self):
+ def test_load_djpeg(self) -> None:
with Image.open(TEST_FILE) as img:
img.load_djpeg()
assert_image_similar_tofile(img, TEST_FILE, 5)
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
- def test_save_cjpeg(self, tmp_path):
+ def test_save_cjpeg(self, tmp_path: Path) -> None:
with Image.open(TEST_FILE) as img:
tempfile = str(tmp_path / "temp.jpg")
JpegImagePlugin._save_cjpeg(img, 0, tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17)
- def test_no_duplicate_0x1001_tag(self):
+ def test_no_duplicate_0x1001_tag(self) -> None:
# Arrange
tag_ids = {v: k for k, v in ExifTags.TAGS.items()}
@@ -665,7 +701,7 @@ class TestFileJpeg:
assert tag_ids["RelatedImageWidth"] == 0x1001
assert tag_ids["RelatedImageLength"] == 0x1002
- def test_MAXBLOCK_scaling(self, tmp_path):
+ def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None:
im = self.gen_random_image((512, 512))
f = str(tmp_path / "temp.jpeg")
im.save(f, quality=100, optimize=True)
@@ -676,7 +712,7 @@ class TestFileJpeg:
reloaded.save(f, quality="keep", progressive=True)
reloaded.save(f, quality="keep", optimize=True)
- def test_bad_mpo_header(self):
+ def test_bad_mpo_header(self) -> None:
"""Treat unknown MPO as JPEG"""
# Arrange
@@ -688,20 +724,20 @@ class TestFileJpeg:
assert im.format == "JPEG"
@pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr"))
- def test_save_correct_modes(self, mode):
+ def test_save_correct_modes(self, mode) -> None:
out = BytesIO()
img = Image.new(mode, (20, 20))
img.save(out, "JPEG")
@pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P"))
- def test_save_wrong_modes(self, mode):
+ def test_save_wrong_modes(self, mode) -> None:
# ref https://github.com/python-pillow/Pillow/issues/2005
out = BytesIO()
img = Image.new(mode, (20, 20))
with pytest.raises(OSError):
img.save(out, "JPEG")
- def test_save_tiff_with_dpi(self, tmp_path):
+ def test_save_tiff_with_dpi(self, tmp_path: Path) -> None:
# Arrange
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/hopper.tif") as im:
@@ -713,7 +749,7 @@ class TestFileJpeg:
reloaded.load()
assert im.info["dpi"] == reloaded.info["dpi"]
- def test_save_dpi_rounding(self, tmp_path):
+ def test_save_dpi_rounding(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.jpg")
with Image.open("Tests/images/hopper.jpg") as im:
im.save(outfile, dpi=(72.2, 72.2))
@@ -726,7 +762,7 @@ class TestFileJpeg:
with Image.open(outfile) as reloaded:
assert reloaded.info["dpi"] == (73, 73)
- def test_dpi_tuple_from_exif(self):
+ def test_dpi_tuple_from_exif(self) -> None:
# Arrange
# This Photoshop CC 2017 image has DPI in EXIF not metadata
# EXIF XResolution is (2000000, 10000)
@@ -734,7 +770,7 @@ class TestFileJpeg:
# Act / Assert
assert im.info.get("dpi") == (200, 200)
- def test_dpi_int_from_exif(self):
+ def test_dpi_int_from_exif(self) -> None:
# Arrange
# This image has DPI in EXIF not metadata
# EXIF XResolution is 72
@@ -742,7 +778,7 @@ class TestFileJpeg:
# Act / Assert
assert im.info.get("dpi") == (72, 72)
- def test_dpi_from_dpcm_exif(self):
+ def test_dpi_from_dpcm_exif(self) -> None:
# Arrange
# This is photoshop-200dpi.jpg with EXIF resolution unit set to cm:
# exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg
@@ -750,7 +786,7 @@ class TestFileJpeg:
# Act / Assert
assert im.info.get("dpi") == (508, 508)
- def test_dpi_exif_zero_division(self):
+ def test_dpi_exif_zero_division(self) -> None:
# Arrange
# This is photoshop-200dpi.jpg with EXIF resolution set to 0/0:
# exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg
@@ -759,7 +795,7 @@ class TestFileJpeg:
# This should return the default, and not raise a ZeroDivisionError
assert im.info.get("dpi") == (72, 72)
- def test_dpi_exif_string(self):
+ def test_dpi_exif_string(self) -> None:
# Arrange
# 0x011A tag in this exif contains string '300300\x02'
with Image.open("Tests/images/broken_exif_dpi.jpg") as im:
@@ -767,14 +803,14 @@ class TestFileJpeg:
# This should return the default
assert im.info.get("dpi") == (72, 72)
- def test_dpi_exif_truncated(self):
+ def test_dpi_exif_truncated(self) -> None:
# Arrange
with Image.open("Tests/images/truncated_exif_dpi.jpg") as im:
# Act / Assert
# This should return the default
assert im.info.get("dpi") == (72, 72)
- def test_no_dpi_in_exif(self):
+ def test_no_dpi_in_exif(self) -> None:
# Arrange
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
# exiftool "-*resolution*"= photoshop-200dpi.jpg
@@ -784,7 +820,7 @@ class TestFileJpeg:
# https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72)
- def test_invalid_exif(self):
+ def test_invalid_exif(self) -> None:
# This is no-dpi-in-exif with the tiff header of the exif block
# hexedited from MM * to FF FF FF FF
with Image.open("Tests/images/invalid-exif.jpg") as im:
@@ -795,7 +831,7 @@ class TestFileJpeg:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_exif_x_resolution(self, tmp_path):
+ def test_exif_x_resolution(self, tmp_path: Path) -> None:
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
assert exif[282] == 180
@@ -807,14 +843,14 @@ class TestFileJpeg:
with Image.open(out) as reloaded:
assert reloaded.getexif()[282] == 180
- def test_invalid_exif_x_resolution(self):
+ def test_invalid_exif_x_resolution(self) -> None:
# When no x or y resolution is defined in EXIF
with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im:
# This should return the default, and not a ValueError or
# OSError for an unidentified image.
assert im.info.get("dpi") == (72, 72)
- def test_ifd_offset_exif(self):
+ def test_ifd_offset_exif(self) -> None:
# Arrange
# This image has been manually hexedited to have an IFD offset of 10,
# in contrast to normal 8
@@ -822,10 +858,14 @@ class TestFileJpeg:
# Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09"
+ def test_multiple_exif(self) -> None:
+ with Image.open("Tests/images/multiple_exif.jpg") as im:
+ assert im.info["exif"] == b"Exif\x00\x00firstsecond"
+
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_photoshop(self):
+ def test_photoshop(self) -> None:
with Image.open("Tests/images/photoshop-200dpi.jpg") as im:
assert im.info["photoshop"][0x03ED] == {
"XResolution": 200.0,
@@ -842,14 +882,14 @@ class TestFileJpeg:
with Image.open("Tests/images/app13.jpg") as im:
assert "photoshop" not in im.info
- def test_photoshop_malformed_and_multiple(self):
+ def test_photoshop_malformed_and_multiple(self) -> None:
with Image.open("Tests/images/app13-multiple.jpg") as im:
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"]
assert [65504, 24] == apps_13_lengths
- def test_adobe_transform(self):
+ def test_adobe_transform(self) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im.info["adobe_transform"] == 1
@@ -863,11 +903,11 @@ class TestFileJpeg:
assert "adobe" in im.info
assert "adobe_transform" not in im.info
- def test_icc_after_SOF(self):
+ def test_icc_after_SOF(self) -> None:
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile"
- def test_jpeg_magic_number(self):
+ def test_jpeg_magic_number(self) -> None:
size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer.max_pos = 0
@@ -886,7 +926,7 @@ class TestFileJpeg:
# Assert the entire file has not been read
assert 0 < buffer.max_pos < size
- def test_getxmp(self):
+ def test_getxmp(self) -> None:
with Image.open("Tests/images/xmp_test.jpg") as im:
if ElementTree is None:
with pytest.warns(
@@ -915,7 +955,7 @@ class TestFileJpeg:
with Image.open("Tests/images/hopper.jpg") as im:
assert im.getxmp() == {}
- def test_getxmp_no_prefix(self):
+ def test_getxmp_no_prefix(self) -> None:
with Image.open("Tests/images/xmp_no_prefix.jpg") as im:
if ElementTree is None:
with pytest.warns(
@@ -926,7 +966,7 @@ class TestFileJpeg:
else:
assert im.getxmp() == {"xmpmeta": {"key": "value"}}
- def test_getxmp_padded(self):
+ def test_getxmp_padded(self) -> None:
with Image.open("Tests/images/xmp_padded.jpg") as im:
if ElementTree is None:
with pytest.warns(
@@ -938,7 +978,7 @@ class TestFileJpeg:
assert im.getxmp() == {"xmpmeta": None}
@pytest.mark.timeout(timeout=1)
- def test_eof(self):
+ def test_eof(self) -> None:
# Even though this decoder never says that it is finished
# the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
@@ -961,14 +1001,36 @@ class TestFileJpeg:
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
- def test_repr_jpeg(self):
+ def test_separate_tables(self) -> None:
+ im = hopper()
+ data = [] # [interchange, tables-only, image-only]
+ for streamtype in range(3):
+ out = BytesIO()
+ im.save(out, format="JPEG", streamtype=streamtype)
+ data.append(out.getvalue())
+
+ # SOI, EOI
+ for marker in b"\xff\xd8", b"\xff\xd9":
+ assert marker in data[1] and marker in data[2]
+ # DHT, DQT
+ for marker in b"\xff\xc4", b"\xff\xdb":
+ assert marker in data[1] and marker not in data[2]
+ # SOF0, SOS, APP0 (JFIF header)
+ for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
+ assert marker not in data[1] and marker in data[2]
+
+ with Image.open(BytesIO(data[0])) as interchange_im:
+ with Image.open(BytesIO(data[1] + data[2])) as combined_im:
+ assert_image_equal(interchange_im, combined_im)
+
+ def test_repr_jpeg(self) -> None:
im = hopper()
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17)
- def test_repr_jpeg_error_returns_none(self):
+ def test_repr_jpeg_error_returns_none(self) -> None:
im = hopper("F")
assert im._repr_jpeg_() is None
@@ -977,7 +1039,7 @@ class TestFileJpeg:
@pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg")
class TestFileCloseW32:
- def test_fd_leak(self, tmp_path):
+ def test_fd_leak(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.jpg")
with Image.open("Tests/images/hopper.jpg") as im:
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index 99df26fc9..e3f1fa8fd 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -1,6 +1,9 @@
+from __future__ import annotations
+
import os
import re
from io import BytesIO
+from pathlib import Path
import pytest
@@ -44,7 +47,7 @@ def roundtrip(im, **options):
return im
-def test_sanity():
+def test_sanity() -> None:
# Internal version number
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000"))
@@ -57,20 +60,20 @@ def test_sanity():
assert im.get_format_mimetype() == "image/jp2"
-def test_jpf():
+def test_jpf() -> None:
with Image.open("Tests/images/balloon.jpf") as im:
assert im.format == "JPEG2000"
assert im.get_format_mimetype() == "image/jpx"
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file)
-def test_bytesio():
+def test_bytesio() -> None:
with open("Tests/images/test-card-lossless.jp2", "rb") as f:
data = BytesIO(f.read())
assert_image_similar_tofile(test_card, data, 1.0e-3)
@@ -80,7 +83,7 @@ def test_bytesio():
# PIL (they were made using Adobe Photoshop)
-def test_lossless(tmp_path):
+def test_lossless(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")
@@ -88,54 +91,54 @@ def test_lossless(tmp_path):
assert_image_similar(im, test_card, 1.0e-3)
-def test_lossy_tiled():
+def test_lossy_tiled() -> None:
assert_image_similar_tofile(
test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0
)
-def test_lossless_rt():
+def test_lossless_rt() -> None:
im = roundtrip(test_card)
assert_image_equal(im, test_card)
-def test_lossy_rt():
+def test_lossy_rt() -> None:
im = roundtrip(test_card, quality_layers=[20])
assert_image_similar(im, test_card, 2.0)
-def test_tiled_rt():
+def test_tiled_rt() -> None:
im = roundtrip(test_card, tile_size=(128, 128))
assert_image_equal(im, test_card)
-def test_tiled_offset_rt():
+def test_tiled_offset_rt() -> None:
im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32))
assert_image_equal(im, test_card)
-def test_tiled_offset_too_small():
+def test_tiled_offset_too_small() -> None:
with pytest.raises(ValueError):
roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32))
-def test_irreversible_rt():
+def test_irreversible_rt() -> None:
im = roundtrip(test_card, irreversible=True, quality_layers=[20])
assert_image_similar(im, test_card, 2.0)
-def test_prog_qual_rt():
+def test_prog_qual_rt() -> None:
im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP")
assert_image_similar(im, test_card, 2.0)
-def test_prog_res_rt():
+def test_prog_res_rt() -> None:
im = roundtrip(test_card, num_resolutions=8, progression="RLCP")
assert_image_equal(im, test_card)
@pytest.mark.parametrize("num_resolutions", range(2, 6))
-def test_default_num_resolutions(num_resolutions):
+def test_default_num_resolutions(num_resolutions) -> None:
d = 1 << (num_resolutions - 1)
im = test_card.resize((d - 1, d - 1))
with pytest.raises(OSError):
@@ -144,7 +147,7 @@ def test_default_num_resolutions(num_resolutions):
assert_image_equal(im, reloaded)
-def test_reduce():
+def test_reduce() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
assert callable(im.reduce)
@@ -158,7 +161,7 @@ def test_reduce():
assert im.size == (40, 30)
-def test_load_dpi():
+def test_load_dpi() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
assert im.info["dpi"] == (71.9836, 71.9836)
@@ -166,7 +169,7 @@ def test_load_dpi():
assert "dpi" not in im.info
-def test_restricted_icc_profile():
+def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
# JPEG2000 image with a restricted ICC profile and a known colorspace
@@ -176,7 +179,7 @@ def test_restricted_icc_profile():
ImageFile.LOAD_TRUNCATED_IMAGES = False
-def test_header_errors():
+def test_header_errors() -> None:
for path in (
"Tests/images/invalid_header_length.jp2",
"Tests/images/not_enough_data.jp2",
@@ -190,7 +193,7 @@ def test_header_errors():
pass
-def test_layers_type(tmp_path):
+def test_layers_type(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp_layers.jp2")
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
test_card.save(outfile, quality_layers=quality_layers)
@@ -200,7 +203,7 @@ def test_layers_type(tmp_path):
test_card.save(outfile, quality_layers=quality_layers)
-def test_layers():
+def test_layers() -> None:
out = BytesIO()
test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP")
out.seek(0)
@@ -230,7 +233,7 @@ def test_layers():
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
),
)
-def test_no_jp2(name, args, offset, data):
+def test_no_jp2(name, args, offset, data) -> None:
out = BytesIO()
if name:
out.name = name
@@ -239,7 +242,7 @@ def test_no_jp2(name, args, offset, data):
assert out.read(2) == data
-def test_mct():
+def test_mct() -> None:
# Three component
for val in (0, 1):
out = BytesIO()
@@ -260,7 +263,7 @@ def test_mct():
assert_image_similar(im, jp2, 1.0e-3)
-def test_sgnd(tmp_path):
+def test_sgnd(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.jp2")
im = Image.new("L", (1, 1))
@@ -275,7 +278,7 @@ def test_sgnd(tmp_path):
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
-def test_rgba(ext):
+def test_rgba(ext) -> None:
# Arrange
with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
# Act
@@ -286,47 +289,47 @@ def test_rgba(ext):
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
-def test_16bit_monochrome_has_correct_mode(ext):
+def test_16bit_monochrome_has_correct_mode(ext) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im:
im.load()
assert im.mode == "I;16"
-def test_16bit_monochrome_jp2_like_tiff():
+def test_16bit_monochrome_jp2_like_tiff() -> None:
with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit:
assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3)
-def test_16bit_monochrome_j2k_like_tiff():
+def test_16bit_monochrome_j2k_like_tiff() -> None:
with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit:
assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3)
-def test_16bit_j2k_roundtrips():
+def test_16bit_j2k_roundtrips() -> None:
with Image.open("Tests/images/16bit.cropped.j2k") as j2k:
im = roundtrip(j2k)
assert_image_equal(im, j2k)
-def test_16bit_jp2_roundtrips():
+def test_16bit_jp2_roundtrips() -> None:
with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
im = roundtrip(jp2)
assert_image_equal(im, jp2)
-def test_issue_6194():
+def test_issue_6194() -> None:
with Image.open("Tests/images/issue_6194.j2k") as im:
assert im.getpixel((5, 5)) == 31
-def test_unbound_local():
+def test_unbound_local() -> None:
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError):
with Image.open("Tests/images/unbound_variable.jp2"):
pass
-def test_parser_feed():
+def test_parser_feed() -> None:
# Arrange
with open("Tests/images/test-card-lossless.jp2", "rb") as f:
data = f.read()
@@ -343,7 +346,7 @@ def test_parser_feed():
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2"))
-def test_subsampling_decode(name):
+def test_subsampling_decode(name) -> None:
test = f"{EXTRA_DIR}/{name}.jp2"
reference = f"{EXTRA_DIR}/{name}.ppm"
@@ -359,7 +362,7 @@ def test_subsampling_decode(name):
assert_image_similar(im, expected, epsilon)
-def test_comment():
+def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
@@ -370,7 +373,7 @@ def test_comment():
pass
-def test_save_comment():
+def test_save_comment() -> None:
for comment in ("Created by Pillow", b"Created by Pillow"):
out = BytesIO()
test_card.save(out, "JPEG2000", comment=comment)
@@ -397,7 +400,7 @@ def test_save_comment():
"Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k",
],
)
-def test_crashes(test_file):
+def test_crashes(test_file) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
# Valgrind should not complain here
@@ -408,7 +411,7 @@ def test_crashes(test_file):
@skip_unless_feature_version("jpg_2000", "2.4.0")
-def test_plt_marker():
+def test_plt_marker() -> None:
# Search the start of the codesteam for PLT
out = BytesIO()
test_card.save(out, "JPEG2000", no_jp2=True, plt=True)
@@ -416,7 +419,7 @@ def test_plt_marker():
while True:
marker = out.read(2)
if not marker:
- assert False, "End of stream without PLT"
+ pytest.fail("End of stream without PLT")
jp2_boxid = _binary.i16be(marker)
if jp2_boxid == 0xFF4F:
@@ -426,7 +429,7 @@ def test_plt_marker():
# PLT
return
elif jp2_boxid == 0xFF93:
- assert False, "SOD without finding PLT first"
+ pytest.fail("SOD without finding PLT first")
hdr = out.read(2)
length = _binary.i16be(hdr)
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index a7394f1bf..1386034e5 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import base64
import io
import itertools
@@ -5,6 +7,7 @@ import os
import re
import sys
from collections import namedtuple
+from pathlib import Path
import pytest
@@ -24,7 +27,7 @@ from .helper import (
@skip_unless_feature("libtiff")
class LibTiffTestCase:
- def _assert_noerr(self, tmp_path, im):
+ def _assert_noerr(self, tmp_path: Path, im) -> None:
"""Helper tests that assert basic sanity about the g4 tiff reading"""
# 1 bit
assert im.mode == "1"
@@ -48,10 +51,10 @@ class LibTiffTestCase:
class TestFileLibTiff(LibTiffTestCase):
- def test_version(self):
+ def test_version(self) -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff"))
- def test_g4_tiff(self, tmp_path):
+ def test_g4_tiff(self, tmp_path: Path) -> None:
"""Test the ordinary file path load path"""
test_file = "Tests/images/hopper_g4_500.tif"
@@ -59,12 +62,12 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
- def test_g4_large(self, tmp_path):
+ def test_g4_large(self, tmp_path: Path) -> None:
test_file = "Tests/images/pport_g4.tif"
with Image.open(test_file) as im:
self._assert_noerr(tmp_path, im)
- def test_g4_tiff_file(self, tmp_path):
+ def test_g4_tiff_file(self, tmp_path: Path) -> None:
"""Testing the string load path"""
test_file = "Tests/images/hopper_g4_500.tif"
@@ -73,7 +76,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
- def test_g4_tiff_bytesio(self, tmp_path):
+ def test_g4_tiff_bytesio(self, tmp_path: Path) -> None:
"""Testing the stringio loading code path"""
test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
@@ -84,7 +87,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
- def test_g4_non_disk_file_object(self, tmp_path):
+ def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
@@ -96,18 +99,18 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
- def test_g4_eq_png(self):
+ def test_g4_eq_png(self) -> None:
"""Checking that we're actually getting the data that we expect"""
with Image.open("Tests/images/hopper_bw_500.png") as png:
assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif")
# see https://github.com/python-pillow/Pillow/issues/279
- def test_g4_fillorder_eq_png(self):
+ def test_g4_fillorder_eq_png(self) -> None:
"""Checking that we're actually getting the data that we expect"""
with Image.open("Tests/images/g4-fillorder-test.tif") as g4:
assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png")
- def test_g4_write(self, tmp_path):
+ def test_g4_write(self, tmp_path: Path) -> None:
"""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:
@@ -126,7 +129,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert orig.tobytes() != reread.tobytes()
- def test_adobe_deflate_tiff(self):
+ def test_adobe_deflate_tiff(self) -> None:
test_file = "Tests/images/tiff_adobe_deflate.tif"
with Image.open(test_file) as im:
assert im.mode == "RGB"
@@ -137,7 +140,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
@pytest.mark.parametrize("legacy_api", (False, True))
- def test_write_metadata(self, legacy_api, tmp_path):
+ def test_write_metadata(self, legacy_api, tmp_path: Path) -> None:
"""Test metadata writing through libtiff"""
f = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper_g4.tif") as img:
@@ -182,7 +185,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert field in reloaded, f"{field} not in metadata"
@pytest.mark.valgrind_known_error(reason="Known invalid metadata")
- def test_additional_metadata(self, tmp_path):
+ def test_additional_metadata(self, tmp_path: Path) -> None:
# these should not crash. Seriously dummy data, most of it doesn't make
# any sense, so we're running up against limits where we're asking
# libtiff to do stupid things.
@@ -239,7 +242,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.WRITE_LIBTIFF = False
- def test_custom_metadata(self, tmp_path):
+ def test_custom_metadata(self, tmp_path: Path) -> None:
tc = namedtuple("test_case", "value,type,supported_by_default")
custom = {
37000 + k: v
@@ -281,7 +284,7 @@ class TestFileLibTiff(LibTiffTestCase):
for libtiff in libtiffs:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
- def check_tags(tiffinfo):
+ def check_tags(tiffinfo) -> None:
im = hopper()
out = str(tmp_path / "temp.tif")
@@ -320,7 +323,7 @@ class TestFileLibTiff(LibTiffTestCase):
)
TiffImagePlugin.WRITE_LIBTIFF = False
- def test_subifd(self, tmp_path):
+ def test_subifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/g4_orientation_6.tif") as im:
im.tag_v2[SUBIFD] = 10000
@@ -328,7 +331,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault
im.save(outfile)
- def test_xmlpacket_tag(self, tmp_path):
+ def test_xmlpacket_tag(self, tmp_path: Path) -> None:
TiffImagePlugin.WRITE_LIBTIFF = True
out = str(tmp_path / "temp.tif")
@@ -339,7 +342,7 @@ class TestFileLibTiff(LibTiffTestCase):
if 700 in reloaded.tag_v2:
assert reloaded.tag_v2[700] == b"xmlpacket tag"
- def test_int_dpi(self, tmp_path):
+ def test_int_dpi(self, tmp_path: Path) -> None:
# issue #1765
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
@@ -349,7 +352,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded:
assert reloaded.info["dpi"] == (72.0, 72.0)
- def test_g3_compression(self, tmp_path):
+ 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")
i.save(out, compression="group3")
@@ -358,7 +361,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert reread.info["compression"] == "group3"
assert_image_equal(reread, i)
- def test_little_endian(self, tmp_path):
+ def test_little_endian(self, tmp_path: Path) -> None:
with Image.open("Tests/images/16bit.deflate.tif") as im:
assert im.getpixel((0, 0)) == 480
assert im.mode == "I;16"
@@ -377,7 +380,7 @@ class TestFileLibTiff(LibTiffTestCase):
# UNDONE - libtiff defaults to writing in native endian, so
# on big endian, we'll get back mode = 'I;16B' here.
- def test_big_endian(self, tmp_path):
+ def test_big_endian(self, tmp_path: Path) -> None:
with Image.open("Tests/images/16bit.MM.deflate.tif") as im:
assert im.getpixel((0, 0)) == 480
assert im.mode == "I;16B"
@@ -394,7 +397,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert reread.info["compression"] == im.info["compression"]
assert reread.getpixel((0, 0)) == 480
- def test_g4_string_info(self, tmp_path):
+ def test_g4_string_info(self, tmp_path: Path) -> None:
"""Tests String data in info directory"""
test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig:
@@ -407,7 +410,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert "temp.tif" == reread.tag_v2[269]
assert "temp.tif" == reread.tag[269][0]
- def test_12bit_rawmode(self):
+ def test_12bit_rawmode(self) -> None:
"""Are we generating the same interpretation
of the image as Imagemagick is?"""
TiffImagePlugin.READ_LIBTIFF = True
@@ -422,7 +425,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/12in16bit.tif")
- def test_blur(self, tmp_path):
+ 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")
@@ -434,7 +437,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, out)
- def test_compressions(self, tmp_path):
+ def test_compressions(self, tmp_path: Path) -> None:
# Test various tiff compressions and assert similar image content but reduced
# file sizes.
im = hopper("RGB")
@@ -460,7 +463,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert size_compressed > size_jpeg
assert size_jpeg > size_jpeg_30
- def test_tiff_jpeg_compression(self, tmp_path):
+ def test_tiff_jpeg_compression(self, tmp_path: Path) -> None:
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
im.save(out, compression="tiff_jpeg")
@@ -468,7 +471,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded:
assert reloaded.info["compression"] == "jpeg"
- def test_tiff_deflate_compression(self, tmp_path):
+ def test_tiff_deflate_compression(self, tmp_path: Path) -> None:
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
im.save(out, compression="tiff_deflate")
@@ -476,7 +479,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded:
assert reloaded.info["compression"] == "tiff_adobe_deflate"
- def test_quality(self, tmp_path):
+ def test_quality(self, tmp_path: Path) -> None:
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
@@ -491,7 +494,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, compression="jpeg", quality=0)
im.save(out, compression="jpeg", quality=100)
- def test_cmyk_save(self, tmp_path):
+ def test_cmyk_save(self, tmp_path: Path) -> None:
im = hopper("CMYK")
out = str(tmp_path / "temp.tif")
@@ -499,7 +502,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, out)
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
- def test_palette_save(self, im, tmp_path):
+ def test_palette_save(self, im, tmp_path: Path) -> None:
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
@@ -511,14 +514,14 @@ class TestFileLibTiff(LibTiffTestCase):
assert len(reloaded.tag_v2[320]) == 768
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
- def test_bw_compression_w_rgb(self, compression, tmp_path):
+ def test_bw_compression_w_rgb(self, compression, tmp_path: Path) -> None:
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
with pytest.raises(OSError):
im.save(out, compression=compression)
- def test_fp_leak(self):
+ def test_fp_leak(self) -> None:
im = Image.open("Tests/images/hopper_g4_500.tif")
fn = im.fp.fileno()
@@ -532,7 +535,7 @@ class TestFileLibTiff(LibTiffTestCase):
with pytest.raises(OSError):
os.close(fn)
- def test_multipage(self):
+ def test_multipage(self) -> None:
# issue #862
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/multipage.tiff") as im:
@@ -555,7 +558,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False
- def test_multipage_nframes(self):
+ def test_multipage_nframes(self) -> None:
# issue #862
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/multipage.tiff") as im:
@@ -568,7 +571,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False
- def test_multipage_seek_backwards(self):
+ def test_multipage_seek_backwards(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/multipage.tiff") as im:
im.seek(1)
@@ -579,14 +582,14 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False
- def test__next(self):
+ def test__next(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/hopper.tif") as im:
assert not im.tag.next
im.load()
assert not im.tag.next
- def test_4bit(self):
+ def test_4bit(self) -> None:
# Arrange
test_file = "Tests/images/hopper_gray_4bpp.tif"
original = hopper("L")
@@ -601,7 +604,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.mode == "L"
assert_image_similar(im, original, 7.3)
- def test_gray_semibyte_per_pixel(self):
+ def test_gray_semibyte_per_pixel(self) -> None:
test_files = (
(
24.8, # epsilon
@@ -634,7 +637,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im2.mode == "L"
assert_image_equal(im, im2)
- def test_save_bytesio(self):
+ def test_save_bytesio(self) -> None:
# PR 1011
# Test TIFF saving to io.BytesIO() object.
@@ -644,7 +647,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Generate test image
pilim = hopper()
- def save_bytesio(compression=None):
+ def save_bytesio(compression=None) -> None:
buffer_io = io.BytesIO()
pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0)
@@ -659,7 +662,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.WRITE_LIBTIFF = False
TiffImagePlugin.READ_LIBTIFF = False
- def test_save_ycbcr(self, tmp_path):
+ def test_save_ycbcr(self, tmp_path: Path) -> None:
im = hopper("YCbCr")
outfile = str(tmp_path / "temp.tif")
im.save(outfile, compression="jpeg")
@@ -668,7 +671,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
- def test_exif_ifd(self, tmp_path):
+ def test_exif_ifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert im.tag_v2[34665] == 125456
@@ -678,7 +681,7 @@ class TestFileLibTiff(LibTiffTestCase):
if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456
- def test_crashing_metadata(self, tmp_path):
+ def test_crashing_metadata(self, tmp_path: Path) -> None:
# issue 1597
with Image.open("Tests/images/rdf.tif") as im:
out = str(tmp_path / "temp.tif")
@@ -688,7 +691,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, format="TIFF")
TiffImagePlugin.WRITE_LIBTIFF = False
- def test_page_number_x_0(self, tmp_path):
+ def test_page_number_x_0(self, tmp_path: Path) -> None:
# Issue 973
# Test TIFF with tag 297 (Page Number) having value of 0 0.
# The first number is the current page number.
@@ -702,7 +705,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not divide by zero
im.save(outfile)
- def test_fd_duplication(self, tmp_path):
+ def test_fd_duplication(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/1651
tmpfile = str(tmp_path / "temp.tif")
@@ -716,7 +719,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise PermissionError.
os.remove(tmpfile)
- def test_read_icc(self):
+ def test_read_icc(self) -> None:
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc = img.info.get("icc_profile")
assert icc is not None
@@ -727,8 +730,8 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False
assert icc == icc_libtiff
- def test_write_icc(self, tmp_path):
- def check_write(libtiff):
+ def test_write_icc(self, tmp_path: Path) -> None:
+ def check_write(libtiff) -> None:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
@@ -747,7 +750,7 @@ class TestFileLibTiff(LibTiffTestCase):
for libtiff in libtiffs:
check_write(libtiff)
- def test_multipage_compression(self):
+ def test_multipage_compression(self) -> None:
with Image.open("Tests/images/compression.tif") as im:
im.seek(0)
assert im._compression == "tiff_ccitt"
@@ -763,7 +766,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (10, 10)
im.load()
- def test_save_tiff_with_jpegtables(self, tmp_path):
+ def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None:
# Arrange
outfile = str(tmp_path / "temp.tif")
@@ -775,7 +778,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise UnicodeDecodeError or anything else
im.save(outfile)
- def test_16bit_RGB_tiff(self):
+ def test_16bit_RGB_tiff(self) -> None:
with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im:
assert im.mode == "RGB"
assert im.size == (100, 40)
@@ -791,7 +794,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png")
- def test_16bit_RGBa_tiff(self):
+ def test_16bit_RGBa_tiff(self) -> None:
with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im:
assert im.mode == "RGBA"
assert im.size == (100, 40)
@@ -803,7 +806,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
@skip_unless_feature("jpg")
- def test_gimp_tiff(self):
+ def test_gimp_tiff(self) -> None:
# Read TIFF JPEG images from GIMP [@PIL168]
filename = "Tests/images/pil168.tif"
with Image.open(filename) as im:
@@ -816,14 +819,14 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/pil168.png")
- def test_sampleformat(self):
+ def test_sampleformat(self) -> None:
# https://github.com/python-pillow/Pillow/issues/1466
with Image.open("Tests/images/copyleft.tiff") as im:
assert im.mode == "RGB"
assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB")
- def test_sampleformat_write(self, tmp_path):
+ def test_sampleformat_write(self, tmp_path: Path) -> None:
im = Image.new("F", (1, 1))
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
@@ -872,7 +875,7 @@ class TestFileLibTiff(LibTiffTestCase):
sys.stderr.write(captured.err)
raise
- def test_lzw(self):
+ def test_lzw(self) -> None:
with Image.open("Tests/images/hopper_lzw.tif") as im:
assert im.mode == "RGB"
assert im.size == (128, 128)
@@ -880,12 +883,12 @@ class TestFileLibTiff(LibTiffTestCase):
im2 = hopper()
assert_image_similar(im, im2, 5)
- def test_strip_cmyk_jpeg(self):
+ def test_strip_cmyk_jpeg(self) -> None:
infile = "Tests/images/tiff_strip_cmyk_jpeg.tif"
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5)
- def test_strip_cmyk_16l_jpeg(self):
+ def test_strip_cmyk_16l_jpeg(self) -> None:
infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif"
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5)
@@ -893,7 +896,7 @@ class TestFileLibTiff(LibTiffTestCase):
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_strip_ycbcr_jpeg_2x2_sampling(self):
+ def test_strip_ycbcr_jpeg_2x2_sampling(self) -> None:
infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif"
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2)
@@ -901,12 +904,12 @@ class TestFileLibTiff(LibTiffTestCase):
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_strip_ycbcr_jpeg_1x1_sampling(self):
+ def test_strip_ycbcr_jpeg_1x1_sampling(self) -> None:
infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif"
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01)
- def test_tiled_cmyk_jpeg(self):
+ def test_tiled_cmyk_jpeg(self) -> None:
infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif"
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5)
@@ -914,7 +917,7 @@ class TestFileLibTiff(LibTiffTestCase):
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_tiled_ycbcr_jpeg_1x1_sampling(self):
+ def test_tiled_ycbcr_jpeg_1x1_sampling(self) -> None:
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif"
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01)
@@ -922,45 +925,45 @@ class TestFileLibTiff(LibTiffTestCase):
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_tiled_ycbcr_jpeg_2x2_sampling(self):
+ def test_tiled_ycbcr_jpeg_2x2_sampling(self) -> None:
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif"
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5)
- def test_strip_planar_rgb(self):
+ def test_strip_planar_rgb(self) -> None:
# gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_strip_raw.tif tiff_strip_planar_lzw.tiff
infile = "Tests/images/tiff_strip_planar_lzw.tiff"
with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
- def test_tiled_planar_rgb(self):
+ def test_tiled_planar_rgb(self) -> None:
# gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff
infile = "Tests/images/tiff_tiled_planar_lzw.tiff"
with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
- def test_tiled_planar_16bit_RGB(self):
+ def test_tiled_planar_16bit_RGB(self) -> None:
# gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff
with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png")
- def test_strip_planar_16bit_RGB(self):
+ def test_strip_planar_16bit_RGB(self) -> None:
# gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff
with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png")
- def test_tiled_planar_16bit_RGBa(self):
+ def test_tiled_planar_16bit_RGBa(self) -> None:
# gdal_translate -co TILED=yes \
# -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \
# tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff
with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
- def test_strip_planar_16bit_RGBa(self):
+ def test_strip_planar_16bit_RGBa(self) -> None:
# gdal_translate -co TILED=no \
# -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \
# tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff
@@ -968,7 +971,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
@pytest.mark.parametrize("compression", (None, "jpeg"))
- def test_block_tile_tags(self, compression, tmp_path):
+ def test_block_tile_tags(self, compression, tmp_path: Path) -> None:
im = hopper()
out = str(tmp_path / "temp.tif")
@@ -984,11 +987,11 @@ class TestFileLibTiff(LibTiffTestCase):
for tag in tags:
assert tag not in reloaded.getexif()
- def test_old_style_jpeg(self):
+ def test_old_style_jpeg(self) -> None:
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_open_missing_samplesperpixel(self):
+ def test_open_missing_samplesperpixel(self) -> None:
with Image.open(
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif"
) as im:
@@ -1017,21 +1020,21 @@ class TestFileLibTiff(LibTiffTestCase):
),
],
)
- def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
+ def test_wrong_bits_per_sample(self, file_name, mode, size, tile) -> None:
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
assert im.size == size
assert im.tile == tile
im.load()
- def test_no_rows_per_strip(self):
+ def test_no_rows_per_strip(self) -> None:
# This image does not have a RowsPerStrip TIFF tag
infile = "Tests/images/no_rows_per_strip.tif"
with Image.open(infile) as im:
im.load()
assert im.size == (950, 975)
- def test_orientation(self):
+ def test_orientation(self) -> None:
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:
@@ -1042,7 +1045,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_similar(base_im, im, 0.7)
- def test_exif_transpose(self):
+ def test_exif_transpose(self) -> None:
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:
@@ -1051,7 +1054,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_similar(base_im, im, 0.7)
@pytest.mark.valgrind_known_error(reason="Backtrace in Python Core")
- def test_sampleformat_not_corrupted(self):
+ def test_sampleformat_not_corrupted(self) -> None:
# Assert that a TIFF image with SampleFormat=UINT tag is not corrupted
# when saving to a new file.
# Pillow 6.0 fails with "OSError: cannot identify image file".
@@ -1072,7 +1075,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as im:
im.load()
- def test_realloc_overflow(self):
+ def test_realloc_overflow(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e:
@@ -1083,7 +1086,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
- def test_save_multistrip(self, compression, tmp_path):
+ def test_save_multistrip(self, compression, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif")
im.save(out, compression=compression)
@@ -1093,7 +1096,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert len(im.tag_v2[STRIPOFFSETS]) > 1
@pytest.mark.parametrize("argument", (True, False))
- def test_save_single_strip(self, argument, tmp_path):
+ def test_save_single_strip(self, argument, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif")
@@ -1111,13 +1114,13 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.STRIP_SIZE = 65536
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
- def test_save_zero(self, compression, tmp_path):
+ def test_save_zero(self, compression, tmp_path: Path) -> None:
im = Image.new("RGB", (0, 0))
out = str(tmp_path / "temp.tif")
with pytest.raises(SystemError):
im.save(out, compression=compression)
- def test_save_many_compressed(self, tmp_path):
+ def test_save_many_compressed(self, tmp_path: Path) -> None:
im = hopper()
out = str(tmp_path / "temp.tif")
for _ in range(10000):
@@ -1131,7 +1134,7 @@ class TestFileLibTiff(LibTiffTestCase):
("Tests/images/child_ifd_jpeg.tiff", (20,)),
),
)
- def test_get_child_images(self, path, sizes):
+ def test_get_child_images(self, path, sizes) -> None:
with Image.open(path) as im:
ims = im.get_child_images()
diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py
index 03137c8b6..ac5270eac 100644
--- a/Tests/test_file_libtiff_small.py
+++ b/Tests/test_file_libtiff_small.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
from PIL import Image
@@ -15,7 +18,7 @@ class TestFileLibTiffSmall(LibTiffTestCase):
file just before reading in libtiff. These tests remain
to ensure that it stays fixed."""
- def test_g4_hopper_file(self, tmp_path):
+ def test_g4_hopper_file(self, tmp_path: Path) -> None:
"""Testing the open file load path"""
test_file = "Tests/images/hopper_g4.tif"
@@ -24,7 +27,7 @@ class TestFileLibTiffSmall(LibTiffTestCase):
assert im.size == (128, 128)
self._assert_noerr(tmp_path, im)
- def test_g4_hopper_bytesio(self, tmp_path):
+ def test_g4_hopper_bytesio(self, tmp_path: Path) -> None:
"""Testing the bytesio loading code path"""
test_file = "Tests/images/hopper_g4.tif"
s = BytesIO()
@@ -35,7 +38,7 @@ class TestFileLibTiffSmall(LibTiffTestCase):
assert im.size == (128, 128)
self._assert_noerr(tmp_path, im)
- def test_g4_hopper(self, tmp_path):
+ def test_g4_hopper(self, tmp_path: Path) -> None:
"""The 128x128 lena image failed for some reason."""
test_file = "Tests/images/hopper_g4.tif"
diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py
index 41f22cf0c..2c94fdc39 100644
--- a/Tests/test_file_mcidas.py
+++ b/Tests/test_file_mcidas.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, McIdasImagePlugin
@@ -5,14 +7,14 @@ from PIL import Image, McIdasImagePlugin
from .helper import assert_image_equal_tofile
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
McIdasImagePlugin.McIdasImageFile(invalid_file)
-def test_valid_file():
+def test_valid_file() -> None:
# Arrange
# https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8
# https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/
diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py
index 2588d3a05..9a6f13ea3 100644
--- a/Tests/test_file_mic.py
+++ b/Tests/test_file_mic.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImagePalette
@@ -11,7 +13,7 @@ pytestmark = skip_unless_feature("libtiff")
TEST_FILE = "Tests/images/hopper.mic"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(TEST_FILE) as im:
im.load()
assert im.mode == "RGBA"
@@ -26,22 +28,22 @@ def test_sanity():
assert_image_similar(im, im2, 10)
-def test_n_frames():
+def test_n_frames() -> None:
with Image.open(TEST_FILE) as im:
assert im.n_frames == 1
-def test_is_animated():
+def test_is_animated() -> None:
with Image.open(TEST_FILE) as im:
assert not im.is_animated
-def test_tell():
+def test_tell() -> None:
with Image.open(TEST_FILE) as im:
assert im.tell() == 0
-def test_seek():
+def test_seek() -> None:
with Image.open(TEST_FILE) as im:
im.seek(0)
assert im.tell() == 0
@@ -51,7 +53,7 @@ def test_seek():
assert im.tell() == 0
-def test_close():
+def test_close() -> None:
with Image.open(TEST_FILE) as im:
pass
assert im.ole.fp.closed
@@ -61,7 +63,7 @@ def test_close():
assert im.ole.fp.closed
-def test_invalid_file():
+def test_invalid_file() -> None:
# Test an invalid OLE file
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index 2e921e467..55b04a1e0 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import warnings
from io import BytesIO
@@ -28,7 +30,7 @@ def roundtrip(im, **options):
@pytest.mark.parametrize("test_file", test_files)
-def test_sanity(test_file):
+def test_sanity(test_file) -> None:
with Image.open(test_file) as im:
im.load()
assert im.mode == "RGB"
@@ -37,8 +39,8 @@ def test_sanity(test_file):
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
-def test_unclosed_file():
- def open():
+def test_unclosed_file() -> None:
+ def open() -> None:
im = Image.open(test_files[0])
im.load()
@@ -46,14 +48,14 @@ def test_unclosed_file():
open()
-def test_closed_file():
+def test_closed_file() -> None:
with warnings.catch_warnings():
im = Image.open(test_files[0])
im.load()
im.close()
-def test_seek_after_close():
+def test_seek_after_close() -> None:
im = Image.open(test_files[0])
im.close()
@@ -61,14 +63,14 @@ def test_seek_after_close():
im.seek(1)
-def test_context_manager():
+def test_context_manager() -> None:
with warnings.catch_warnings():
with Image.open(test_files[0]) as im:
im.load()
@pytest.mark.parametrize("test_file", test_files)
-def test_app(test_file):
+def test_app(test_file) -> None:
# Test APP/COM reader (@PIL135)
with Image.open(test_file) as im:
assert im.applist[0][0] == "APP1"
@@ -80,7 +82,7 @@ def test_app(test_file):
@pytest.mark.parametrize("test_file", test_files)
-def test_exif(test_file):
+def test_exif(test_file) -> None:
with Image.open(test_file) as im_original:
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
@@ -91,7 +93,7 @@ def test_exif(test_file):
assert info[34665] == 188
-def test_frame_size():
+def test_frame_size() -> None:
# This image has been hexedited to contain a different size
# in the EXIF data of the second frame
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
@@ -104,7 +106,7 @@ def test_frame_size():
assert im.size == (640, 480)
-def test_ignore_frame_size():
+def test_ignore_frame_size() -> None:
# Ignore the different size of the second frame
# since this is not a "Large Thumbnail" image
with Image.open("Tests/images/ignore_frame_size.mpo") as im:
@@ -118,7 +120,7 @@ def test_ignore_frame_size():
assert im.size == (64, 64)
-def test_parallax():
+def test_parallax() -> None:
# Nintendo
with Image.open("Tests/images/sugarshack.mpo") as im:
exif = im.getexif()
@@ -131,7 +133,7 @@ def test_parallax():
assert exif.get_ifd(0x927C)[0xB211] == -3.125
-def test_reload_exif_after_seek():
+def test_reload_exif_after_seek() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
exif = im.getexif()
del exif[296]
@@ -141,14 +143,14 @@ def test_reload_exif_after_seek():
@pytest.mark.parametrize("test_file", test_files)
-def test_mp(test_file):
+def test_mp(test_file) -> None:
with Image.open(test_file) as im:
mpinfo = im._getmp()
assert mpinfo[45056] == b"0100"
assert mpinfo[45057] == 2
-def test_mp_offset():
+def test_mp_offset() -> None:
# This image has been manually hexedited to have an IFD offset of 10
# in APP2 data, in contrast to normal 8
with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im:
@@ -157,7 +159,7 @@ def test_mp_offset():
assert mpinfo[45057] == 2
-def test_mp_no_data():
+def test_mp_no_data() -> None:
# This image has been manually hexedited to have the second frame
# beyond the end of the file
with Image.open("Tests/images/sugarshack_no_data.mpo") as im:
@@ -166,7 +168,7 @@ def test_mp_no_data():
@pytest.mark.parametrize("test_file", test_files)
-def test_mp_attribute(test_file):
+def test_mp_attribute(test_file) -> None:
with Image.open(test_file) as im:
mpinfo = im._getmp()
for frame_number, mpentry in enumerate(mpinfo[0xB002]):
@@ -183,7 +185,7 @@ def test_mp_attribute(test_file):
@pytest.mark.parametrize("test_file", test_files)
-def test_seek(test_file):
+def test_seek(test_file) -> None:
with Image.open(test_file) as im:
assert im.tell() == 0
# prior to first image raises an error, both blatant and borderline
@@ -207,13 +209,13 @@ def test_seek(test_file):
assert im.tell() == 0
-def test_n_frames():
+def test_n_frames() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
assert im.n_frames == 2
assert im.is_animated
-def test_eoferror():
+def test_eoferror() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
n_frames = im.n_frames
@@ -227,7 +229,7 @@ def test_eoferror():
@pytest.mark.parametrize("test_file", test_files)
-def test_image_grab(test_file):
+def test_image_grab(test_file) -> None:
with Image.open(test_file) as im:
assert im.tell() == 0
im0 = im.tobytes()
@@ -242,7 +244,7 @@ def test_image_grab(test_file):
@pytest.mark.parametrize("test_file", test_files)
-def test_save(test_file):
+def test_save(test_file) -> None:
with Image.open(test_file) as im:
assert im.tell() == 0
jpg0 = roundtrip(im)
@@ -253,7 +255,7 @@ def test_save(test_file):
assert_image_similar(im, jpg1, 30)
-def test_save_all():
+def test_save_all() -> None:
for test_file in test_files:
with Image.open(test_file) as im:
im_reloaded = roundtrip(im, save_all=True)
diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py
index 497052b05..f9f81d114 100644
--- a/Tests/test_file_msp.py
+++ b/Tests/test_file_msp.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
import os
+from pathlib import Path
import pytest
@@ -11,7 +14,7 @@ EXTRA_DIR = "Tests/images/picins"
YA_EXTRA_DIR = "Tests/images/msp"
-def test_sanity(tmp_path):
+def test_sanity(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.msp")
hopper("1").save(test_file)
@@ -23,14 +26,14 @@ def test_sanity(tmp_path):
assert im.format == "MSP"
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
MspImagePlugin.MspImageFile(invalid_file)
-def test_bad_checksum():
+def test_bad_checksum() -> None:
# Arrange
# This was created by forcing Pillow to save with checksum=0
bad_checksum = "Tests/images/hopper_bad_checksum.msp"
@@ -40,7 +43,7 @@ def test_bad_checksum():
MspImagePlugin.MspImageFile(bad_checksum)
-def test_open_windows_v1():
+def test_open_windows_v1() -> None:
# Arrange
# Act
with Image.open(TEST_FILE) as im:
@@ -49,7 +52,7 @@ def test_open_windows_v1():
assert isinstance(im, MspImagePlugin.MspImageFile)
-def _assert_file_image_equal(source_path, target_path):
+def _assert_file_image_equal(source_path, target_path) -> None:
with Image.open(source_path) as im:
assert_image_equal_tofile(im, target_path)
@@ -57,7 +60,7 @@ def _assert_file_image_equal(source_path, target_path):
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
-def test_open_windows_v2():
+def test_open_windows_v2() -> None:
files = (
os.path.join(EXTRA_DIR, f)
for f in os.listdir(EXTRA_DIR)
@@ -70,7 +73,7 @@ def test_open_windows_v2():
@pytest.mark.skipif(
not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed"
)
-def test_msp_v2():
+def test_msp_v2() -> None:
for f in os.listdir(YA_EXTRA_DIR):
if ".MSP" not in f:
continue
@@ -78,7 +81,7 @@ def test_msp_v2():
_assert_file_image_equal(path, path.replace(".MSP", ".png"))
-def test_cannot_save_wrong_mode(tmp_path):
+def test_cannot_save_wrong_mode(tmp_path: Path) -> None:
# Arrange
im = hopper()
filename = str(tmp_path / "temp.msp")
diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py
index be7c8d0c8..55041a4b2 100644
--- a/Tests/test_file_palm.py
+++ b/Tests/test_file_palm.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
import os.path
import subprocess
+from pathlib import Path
import pytest
@@ -8,7 +11,7 @@ from PIL import Image
from .helper import assert_image_equal, hopper, magick_command
-def helper_save_as_palm(tmp_path, mode):
+def helper_save_as_palm(tmp_path: Path, mode) -> None:
# Arrange
im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".palm"))
@@ -21,17 +24,16 @@ def helper_save_as_palm(tmp_path, mode):
assert os.path.getsize(outfile) > 0
-def open_with_magick(magick, tmp_path, f):
+def open_with_magick(magick, tmp_path: Path, f):
outfile = str(tmp_path / "temp.png")
rc = subprocess.call(
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
)
- if rc:
- raise OSError
+ assert not rc
return Image.open(outfile)
-def roundtrip(tmp_path, mode):
+def roundtrip(tmp_path: Path, mode) -> None:
magick = magick_command()
if not magick:
return
@@ -44,7 +46,7 @@ def roundtrip(tmp_path, mode):
assert_image_equal(converted, im)
-def test_monochrome(tmp_path):
+def test_monochrome(tmp_path: Path) -> None:
# Arrange
mode = "1"
@@ -54,7 +56,7 @@ def test_monochrome(tmp_path):
@pytest.mark.xfail(reason="Palm P image is wrong")
-def test_p_mode(tmp_path):
+def test_p_mode(tmp_path: Path) -> None:
# Arrange
mode = "P"
@@ -64,6 +66,6 @@ def test_p_mode(tmp_path):
@pytest.mark.parametrize("mode", ("L", "RGB"))
-def test_oserror(tmp_path, mode):
+def test_oserror(tmp_path: Path, mode) -> None:
with pytest.raises(OSError):
helper_save_as_palm(tmp_path, mode)
diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py
index dc45a48c1..81a316fc1 100644
--- a/Tests/test_file_pcd.py
+++ b/Tests/test_file_pcd.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from PIL import Image
-def test_load_raw():
+def test_load_raw() -> None:
with Image.open("Tests/images/hopper.pcd") as im:
im.load() # should not segfault.
diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py
index 485adf785..a2486be40 100644
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImageFile, PcxImagePlugin
@@ -5,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin
from .helper import assert_image_equal, hopper
-def _roundtrip(tmp_path, im):
+def _roundtrip(tmp_path: Path, im) -> None:
f = str(tmp_path / "temp.pcx")
im.save(f)
with Image.open(f) as im2:
@@ -16,7 +20,7 @@ def _roundtrip(tmp_path, im):
assert_image_equal(im2, im)
-def test_sanity(tmp_path):
+def test_sanity(tmp_path: Path) -> None:
for mode in ("1", "L", "P", "RGB"):
_roundtrip(tmp_path, hopper(mode))
@@ -32,7 +36,7 @@ def test_sanity(tmp_path):
im.save(f)
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
@@ -40,7 +44,7 @@ def test_invalid_file():
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
-def test_odd(tmp_path, mode):
+def test_odd(tmp_path: Path, mode) -> None:
# See issue #523, odd sized images should have a stride that's even.
# Not that ImageMagick or GIMP write PCX that way.
# We were not handling properly.
@@ -49,7 +53,7 @@ def test_odd(tmp_path, mode):
_roundtrip(tmp_path, hopper(mode).resize((511, 511)))
-def test_odd_read():
+def test_odd_read() -> None:
# Reading an image with an odd stride, making it malformed
with Image.open("Tests/images/odd_stride.pcx") as im:
im.load()
@@ -57,7 +61,7 @@ def test_odd_read():
assert im.size == (371, 150)
-def test_pil184():
+def test_pil184() -> None:
# Check reading of files where xmin/xmax is not zero.
test_file = "Tests/images/pil184.pcx"
@@ -69,7 +73,7 @@ def test_pil184():
assert im.histogram()[0] + im.histogram()[255] == 447 * 144
-def test_1px_width(tmp_path):
+def test_1px_width(tmp_path: Path) -> None:
im = Image.new("L", (1, 256))
px = im.load()
for y in range(256):
@@ -77,7 +81,7 @@ def test_1px_width(tmp_path):
_roundtrip(tmp_path, im)
-def test_large_count(tmp_path):
+def test_large_count(tmp_path: Path) -> None:
im = Image.new("L", (256, 1))
px = im.load()
for x in range(256):
@@ -85,7 +89,7 @@ def test_large_count(tmp_path):
_roundtrip(tmp_path, im)
-def _test_buffer_overflow(tmp_path, im, size=1024):
+def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None:
_last = ImageFile.MAXBLOCK
ImageFile.MAXBLOCK = size
try:
@@ -94,7 +98,7 @@ def _test_buffer_overflow(tmp_path, im, size=1024):
ImageFile.MAXBLOCK = _last
-def test_break_in_count_overflow(tmp_path):
+def test_break_in_count_overflow(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
for y in range(4):
@@ -103,7 +107,7 @@ def test_break_in_count_overflow(tmp_path):
_test_buffer_overflow(tmp_path, im)
-def test_break_one_in_loop(tmp_path):
+def test_break_one_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
for y in range(5):
@@ -112,7 +116,7 @@ def test_break_one_in_loop(tmp_path):
_test_buffer_overflow(tmp_path, im)
-def test_break_many_in_loop(tmp_path):
+def test_break_many_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
for y in range(4):
@@ -123,7 +127,7 @@ def test_break_many_in_loop(tmp_path):
_test_buffer_overflow(tmp_path, im)
-def test_break_one_at_end(tmp_path):
+def test_break_one_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
for y in range(5):
@@ -133,7 +137,7 @@ def test_break_one_at_end(tmp_path):
_test_buffer_overflow(tmp_path, im)
-def test_break_many_at_end(tmp_path):
+def test_break_many_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
for y in range(5):
@@ -145,7 +149,7 @@ def test_break_many_at_end(tmp_path):
_test_buffer_overflow(tmp_path, im)
-def test_break_padding(tmp_path):
+def test_break_padding(tmp_path: Path) -> None:
im = Image.new("L", (257, 5))
px = im.load()
for y in range(5):
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index ffc392d6b..65a93c138 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -1,8 +1,11 @@
+from __future__ import annotations
+
import io
import os
import os.path
import tempfile
import time
+from pathlib import Path
import pytest
@@ -11,7 +14,7 @@ from PIL import Image, PdfParser, features
from .helper import hopper, mark_if_feature_version, skip_unless_feature
-def helper_save_as_pdf(tmp_path, mode, **kwargs):
+def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
# Arrange
im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
@@ -38,17 +41,17 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs):
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
-def test_save(tmp_path, mode):
+def test_save(tmp_path: Path, mode) -> None:
helper_save_as_pdf(tmp_path, mode)
@skip_unless_feature("jpg_2000")
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
-def test_save_alpha(tmp_path, mode):
+def test_save_alpha(tmp_path: Path, mode) -> None:
helper_save_as_pdf(tmp_path, mode)
-def test_p_alpha(tmp_path):
+def test_p_alpha(tmp_path: Path) -> None:
# Arrange
outfile = str(tmp_path / "temp.pdf")
with Image.open("Tests/images/pil123p.png") as im:
@@ -64,7 +67,7 @@ def test_p_alpha(tmp_path):
assert b"\n/SMask " in contents
-def test_monochrome(tmp_path):
+def test_monochrome(tmp_path: Path) -> None:
# Arrange
mode = "1"
@@ -73,7 +76,7 @@ def test_monochrome(tmp_path):
assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000)
-def test_unsupported_mode(tmp_path):
+def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("PA")
outfile = str(tmp_path / "temp_PA.pdf")
@@ -81,7 +84,7 @@ def test_unsupported_mode(tmp_path):
im.save(outfile)
-def test_resolution(tmp_path):
+def test_resolution(tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
@@ -109,7 +112,7 @@ def test_resolution(tmp_path):
{"dpi": (75, 150), "resolution": 200},
),
)
-def test_dpi(params, tmp_path):
+def test_dpi(params, tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
@@ -133,7 +136,7 @@ def test_dpi(params, tmp_path):
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
-def test_save_all(tmp_path):
+def test_save_all(tmp_path: Path) -> None:
# Single frame image
helper_save_as_pdf(tmp_path, "RGB", save_all=True)
@@ -169,7 +172,7 @@ def test_save_all(tmp_path):
assert os.path.getsize(outfile) > 0
-def test_multiframe_normal_save(tmp_path):
+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")
@@ -179,7 +182,7 @@ def test_multiframe_normal_save(tmp_path):
assert os.path.getsize(outfile) > 0
-def test_pdf_open(tmp_path):
+def test_pdf_open(tmp_path: Path) -> None:
# fail on a buffer full of null bytes
with pytest.raises(PdfParser.PdfFormatError):
PdfParser.PdfParser(buf=bytearray(65536))
@@ -216,14 +219,14 @@ def test_pdf_open(tmp_path):
assert not hopper_pdf.should_close_file
-def test_pdf_append_fails_on_nonexistent_file():
+def test_pdf_append_fails_on_nonexistent_file() -> None:
im = hopper("RGB")
with tempfile.TemporaryDirectory() as temp_dir:
with pytest.raises(OSError):
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
-def check_pdf_pages_consistency(pdf):
+def check_pdf_pages_consistency(pdf) -> None:
pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info
assert b"Kids" in pages_info
@@ -241,7 +244,7 @@ def check_pdf_pages_consistency(pdf):
assert kids_not_used == []
-def test_pdf_append(tmp_path):
+def test_pdf_append(tmp_path: Path) -> None:
# make a PDF file
pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser")
@@ -292,7 +295,7 @@ def test_pdf_append(tmp_path):
check_pdf_pages_consistency(pdf)
-def test_pdf_info(tmp_path):
+def test_pdf_info(tmp_path: Path) -> None:
# make a PDF file
pdf_filename = helper_save_as_pdf(
tmp_path,
@@ -321,7 +324,7 @@ def test_pdf_info(tmp_path):
check_pdf_pages_consistency(pdf)
-def test_pdf_append_to_bytesio():
+def test_pdf_append_to_bytesio() -> None:
im = hopper("RGB")
f = io.BytesIO()
im.save(f, format="PDF")
@@ -336,7 +339,7 @@ def test_pdf_append_to_bytesio():
@pytest.mark.timeout(1)
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
-def test_redos(newline):
+def test_redos(newline) -> None:
malicious = b" trailer<<>>" + newline * 3456
# This particular exception isn't relevant here.
diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py
index 315ea4676..8f208cfbf 100644
--- a/Tests/test_file_pixar.py
+++ b/Tests/test_file_pixar.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, PixarImagePlugin
@@ -7,7 +9,7 @@ from .helper import assert_image_similar, hopper
TEST_FILE = "Tests/images/hopper.pxr"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(TEST_FILE) as im:
im.load()
assert im.mode == "RGB"
@@ -19,7 +21,7 @@ def test_sanity():
assert_image_similar(im, im2, 4.8)
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index 40fc595ad..0f1d96365 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -1,8 +1,11 @@
+from __future__ import annotations
+
import re
import sys
import warnings
import zlib
from io import BytesIO
+from pathlib import Path
import pytest
@@ -77,7 +80,7 @@ class TestFilePng:
png.crc(cid, s)
return chunks
- def test_sanity(self, tmp_path):
+ def test_sanity(self, tmp_path: Path) -> None:
# internal version number
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
@@ -100,13 +103,13 @@ class TestFilePng:
reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im)
- def test_invalid_file(self):
+ def test_invalid_file(self) -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
PngImagePlugin.PngImageFile(invalid_file)
- def test_broken(self):
+ def test_broken(self) -> None:
# Check reading of totally broken files. In this case, the test
# file was checked into Subversion as a text file.
@@ -115,7 +118,7 @@ class TestFilePng:
with Image.open(test_file):
pass
- def test_bad_text(self):
+ def test_bad_text(self) -> None:
# Make sure PIL can read malformed tEXt chunks (@PIL152)
im = load(HEAD + chunk(b"tEXt") + TAIL)
@@ -133,7 +136,7 @@ class TestFilePng:
im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL)
assert im.info == {"spam": "egg\x00"}
- def test_bad_ztxt(self):
+ def test_bad_ztxt(self) -> None:
# Test reading malformed zTXt chunks (python-pillow/Pillow#318)
im = load(HEAD + chunk(b"zTXt") + TAIL)
@@ -154,7 +157,7 @@ class TestFilePng:
im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL)
assert im.info == {"spam": "egg"}
- def test_bad_itxt(self):
+ def test_bad_itxt(self) -> None:
im = load(HEAD + chunk(b"iTXt") + TAIL)
assert im.info == {}
@@ -198,7 +201,7 @@ class TestFilePng:
assert im.info["spam"].lang == "en"
assert im.info["spam"].tkey == "Spam"
- def test_interlace(self):
+ def test_interlace(self) -> None:
test_file = "Tests/images/pil123p.png"
with Image.open(test_file) as im:
assert_image(im, "P", (162, 150))
@@ -213,7 +216,7 @@ class TestFilePng:
im.load()
- def test_load_transparent_p(self):
+ def test_load_transparent_p(self) -> None:
test_file = "Tests/images/pil123p.png"
with Image.open(test_file) as im:
assert_image(im, "P", (162, 150))
@@ -223,7 +226,7 @@ class TestFilePng:
# image has 124 unique alpha values
assert len(im.getchannel("A").getcolors()) == 124
- def test_load_transparent_rgb(self):
+ def test_load_transparent_rgb(self) -> None:
test_file = "Tests/images/rgb_trns.png"
with Image.open(test_file) as im:
assert im.info["transparency"] == (0, 255, 52)
@@ -235,7 +238,7 @@ class TestFilePng:
# image has 876 transparent pixels
assert im.getchannel("A").getcolors()[0][0] == 876
- def test_save_p_transparent_palette(self, tmp_path):
+ def test_save_p_transparent_palette(self, tmp_path: Path) -> None:
in_file = "Tests/images/pil123p.png"
with Image.open(in_file) as im:
# 'transparency' contains a byte string with the opacity for
@@ -256,7 +259,7 @@ class TestFilePng:
# image has 124 unique alpha values
assert len(im.getchannel("A").getcolors()) == 124
- def test_save_p_single_transparency(self, tmp_path):
+ def test_save_p_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/p_trns_single.png"
with Image.open(in_file) as im:
# pixel value 164 is full transparent
@@ -279,7 +282,7 @@ class TestFilePng:
# image has 876 transparent pixels
assert im.getchannel("A").getcolors()[0][0] == 876
- def test_save_p_transparent_black(self, tmp_path):
+ def test_save_p_transparent_black(self, tmp_path: Path) -> None:
# check if solid black image with full transparency
# is supported (check for #1838)
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
@@ -297,7 +300,7 @@ class TestFilePng:
assert_image(im, "RGBA", (10, 10))
assert im.getcolors() == [(100, (0, 0, 0, 0))]
- def test_save_greyscale_transparency(self, tmp_path):
+ def test_save_grayscale_transparency(self, tmp_path: Path) -> None:
for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
in_file = "Tests/images/" + mode.lower() + "_trns.png"
with Image.open(in_file) as im:
@@ -318,13 +321,13 @@ class TestFilePng:
test_im_rgba = test_im.convert("RGBA")
assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
- def test_save_rgb_single_transparency(self, tmp_path):
+ 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")
im.save(test_file)
- def test_load_verify(self):
+ def test_load_verify(self) -> None:
# Check open/load/verify exception (@PIL150)
with Image.open(TEST_PNG_FILE) as im:
@@ -337,7 +340,7 @@ class TestFilePng:
with pytest.raises(RuntimeError):
im.verify()
- def test_verify_struct_error(self):
+ def test_verify_struct_error(self) -> None:
# Check open/load/verify exception (#1755)
# offsets to test, -10: breaks in i32() in read. (OSError)
@@ -353,7 +356,7 @@ class TestFilePng:
with pytest.raises((OSError, SyntaxError)):
im.verify()
- def test_verify_ignores_crc_error(self):
+ def test_verify_ignores_crc_error(self) -> None:
# check ignores crc errors in ancillary chunks
chunk_data = chunk(b"tEXt", b"spam")
@@ -370,7 +373,7 @@ class TestFilePng:
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
- def test_verify_not_ignores_crc_error_in_required_chunk(self):
+ def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None:
# check does not ignore crc errors in required chunks
image_data = MAGIC + IHDR[:-1] + b"q" + TAIL
@@ -382,18 +385,18 @@ class TestFilePng:
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
- def test_roundtrip_dpi(self):
+ def test_roundtrip_dpi(self) -> None:
# Check dpi roundtripping
with Image.open(TEST_PNG_FILE) as im:
im = roundtrip(im, dpi=(100.33, 100.33))
assert im.info["dpi"] == (100.33, 100.33)
- def test_load_float_dpi(self):
+ def test_load_float_dpi(self) -> None:
with Image.open(TEST_PNG_FILE) as im:
assert im.info["dpi"] == (95.9866, 95.9866)
- def test_roundtrip_text(self):
+ def test_roundtrip_text(self) -> None:
# Check text roundtripping
with Image.open(TEST_PNG_FILE) as im:
@@ -405,7 +408,7 @@ class TestFilePng:
assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"}
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
- def test_roundtrip_itxt(self):
+ def test_roundtrip_itxt(self) -> None:
# Check iTXt roundtripping
im = Image.new("RGB", (32, 32))
@@ -421,7 +424,7 @@ class TestFilePng:
assert im.text["eggs"].lang == "en"
assert im.text["eggs"].tkey == "Eggs"
- def test_nonunicode_text(self):
+ def test_nonunicode_text(self) -> None:
# Check so that non-Unicode text is saved as a tEXt rather than iTXt
im = Image.new("RGB", (32, 32))
@@ -430,10 +433,10 @@ class TestFilePng:
im = roundtrip(im, pnginfo=info)
assert isinstance(im.info["Text"], str)
- def test_unicode_text(self):
+ def test_unicode_text(self) -> None:
# Check preservation of non-ASCII characters
- def rt_text(value):
+ def rt_text(value) -> None:
im = Image.new("RGB", (32, 32))
info = PngImagePlugin.PngInfo()
info.add_text("Text", value)
@@ -446,7 +449,7 @@ class TestFilePng:
rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00))
rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined
- def test_scary(self):
+ def test_scary(self) -> None:
# Check reading of evil PNG file. For information, see:
# http://scary.beasts.org/security/CESA-2004-001.txt
# The first byte is removed from pngtest_bad.png
@@ -460,7 +463,7 @@ class TestFilePng:
with Image.open(pngfile):
pass
- def test_trns_rgb(self):
+ def test_trns_rgb(self) -> None:
# Check writing and reading of tRNS chunks for RGB images.
# Independent file sample provided by Sebastian Spaeth.
@@ -475,7 +478,7 @@ class TestFilePng:
im = roundtrip(im, transparency=(0, 1, 2))
assert im.info["transparency"] == (0, 1, 2)
- def test_trns_p(self, tmp_path):
+ def test_trns_p(self, tmp_path: Path) -> None:
# Check writing a transparency of 0, issue #528
im = hopper("P")
im.info["transparency"] = 0
@@ -488,13 +491,13 @@ class TestFilePng:
assert_image_equal(im2.convert("RGBA"), im.convert("RGBA"))
- def test_trns_null(self):
+ def test_trns_null(self) -> None:
# Check reading images with null tRNS value, issue #1239
test_file = "Tests/images/tRNS_null_1x1.png"
with Image.open(test_file) as im:
assert im.info["transparency"] == 0
- def test_save_icc_profile(self):
+ def test_save_icc_profile(self) -> None:
with Image.open("Tests/images/icc_profile_none.png") as im:
assert im.info["icc_profile"] is None
@@ -504,40 +507,40 @@ class TestFilePng:
im = roundtrip(im, icc_profile=expected_icc)
assert im.info["icc_profile"] == expected_icc
- def test_discard_icc_profile(self):
+ def test_discard_icc_profile(self) -> None:
with Image.open("Tests/images/icc_profile.png") as im:
assert "icc_profile" in im.info
im = roundtrip(im, icc_profile=None)
assert "icc_profile" not in im.info
- def test_roundtrip_icc_profile(self):
+ def test_roundtrip_icc_profile(self) -> None:
with Image.open("Tests/images/icc_profile.png") 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):
+ def test_roundtrip_no_icc_profile(self) -> None:
with Image.open("Tests/images/icc_profile_none.png") as im:
assert im.info["icc_profile"] is None
im = roundtrip(im)
assert "icc_profile" not in im.info
- def test_repr_png(self):
+ def test_repr_png(self) -> None:
im = hopper()
with Image.open(BytesIO(im._repr_png_())) as repr_png:
assert repr_png.format == "PNG"
assert_image_equal(im, repr_png)
- def test_repr_png_error_returns_none(self):
+ def test_repr_png_error_returns_none(self) -> None:
im = hopper("F")
assert im._repr_png_() is None
- def test_chunk_order(self, tmp_path):
+ 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")
im.convert("P").save(test_file, dpi=(100, 100))
@@ -558,17 +561,17 @@ class TestFilePng:
# pHYs - before IDAT
assert chunks.index(b"pHYs") < chunks.index(b"IDAT")
- def test_getchunks(self):
+ def test_getchunks(self) -> None:
im = hopper()
chunks = PngImagePlugin.getchunks(im)
assert len(chunks) == 3
- def test_read_private_chunks(self):
+ def test_read_private_chunks(self) -> None:
with Image.open("Tests/images/exif.png") as im:
assert im.private_chunks == [(b"orNT", b"\x01")]
- def test_roundtrip_private_chunk(self):
+ def test_roundtrip_private_chunk(self) -> None:
# Check private chunk roundtripping
with Image.open(TEST_PNG_FILE) as im:
@@ -586,7 +589,7 @@ class TestFilePng:
(b"prIV", b"VALUE3", True),
]
- def test_textual_chunks_after_idat(self):
+ def test_textual_chunks_after_idat(self) -> None:
with Image.open("Tests/images/hopper.png") as im:
assert "comment" in im.text
for k, v in {
@@ -613,7 +616,7 @@ class TestFilePng:
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
- def test_padded_idat(self):
+ def test_padded_idat(self) -> None:
# This image has been manually hexedited
# so that the IDAT chunk has padding at the end
# Set MAXBLOCK to the length of the actual data
@@ -633,7 +636,7 @@ class TestFilePng:
@pytest.mark.parametrize(
"cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT")
)
- def test_truncated_chunks(self, cid):
+ def test_truncated_chunks(self, cid) -> None:
fp = BytesIO()
with PngImagePlugin.PngStream(fp) as png:
with pytest.raises(ValueError):
@@ -643,7 +646,7 @@ class TestFilePng:
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False
- def test_specify_bits(self, tmp_path):
+ def test_specify_bits(self, tmp_path: Path) -> None:
im = hopper("P")
out = str(tmp_path / "temp.png")
@@ -652,7 +655,7 @@ class TestFilePng:
with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 48
- def test_plte_length(self, tmp_path):
+ def test_plte_length(self, tmp_path: Path) -> None:
im = Image.new("P", (1, 1))
im.putpalette((1, 1, 1))
@@ -662,7 +665,7 @@ class TestFilePng:
with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 3
- def test_getxmp(self):
+ def test_getxmp(self) -> None:
with Image.open("Tests/images/color_snakes.png") as im:
if ElementTree is None:
with pytest.warns(
@@ -677,7 +680,7 @@ class TestFilePng:
assert description["PixelXDimension"] == "10"
assert description["subject"]["Seq"] is None
- def test_exif(self):
+ def test_exif(self) -> None:
# With an EXIF chunk
with Image.open("Tests/images/exif.png") as im:
exif = im._getexif()
@@ -703,7 +706,7 @@ class TestFilePng:
exif = im.getexif()
assert exif[274] == 3
- def test_exif_save(self, tmp_path):
+ def test_exif_save(self, tmp_path: Path) -> None:
# Test exif is not saved from info
test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/exif.png") as im:
@@ -723,7 +726,7 @@ class TestFilePng:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_exif_from_jpg(self, tmp_path):
+ 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")
im.save(test_file, exif=im.getexif())
@@ -732,7 +735,7 @@ class TestFilePng:
exif = reloaded._getexif()
assert exif[305] == "Adobe Photoshop CS Macintosh"
- def test_exif_argument(self, tmp_path):
+ def test_exif_argument(self, tmp_path: Path) -> None:
with Image.open(TEST_PNG_FILE) as im:
test_file = str(tmp_path / "temp.png")
im.save(test_file, exif=b"exifstring")
@@ -740,11 +743,11 @@ class TestFilePng:
with Image.open(test_file) as reloaded:
assert reloaded.info["exif"] == b"Exif\x00\x00exifstring"
- def test_tell(self):
+ def test_tell(self) -> None:
with Image.open(TEST_PNG_FILE) as im:
assert im.tell() == 0
- def test_seek(self):
+ def test_seek(self) -> None:
with Image.open(TEST_PNG_FILE) as im:
im.seek(0)
@@ -752,7 +755,7 @@ class TestFilePng:
im.seek(1)
@pytest.mark.parametrize("buffer", (True, False))
- def test_save_stdout(self, buffer):
+ def test_save_stdout(self, buffer) -> None:
old_stdout = sys.stdout
if buffer:
@@ -784,7 +787,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
mem_limit = 2 * 1024 # max increase in K
iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs
- def test_leak_load(self):
+ def test_leak_load(self) -> None:
with open("Tests/images/hopper.png", "rb") as f:
DATA = BytesIO(f.read(16 * 1024))
@@ -792,7 +795,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
with Image.open(DATA) as im:
im.load()
- def core():
+ def core() -> None:
with Image.open(DATA) as im:
im.load()
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index 292642ca9..94f66ee7d 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -1,17 +1,25 @@
+from __future__ import annotations
+
import sys
from io import BytesIO
+from pathlib import Path
import pytest
from PIL import Image, PpmImagePlugin
-from .helper import assert_image_equal_tofile, assert_image_similar, hopper
+from .helper import (
+ assert_image_equal,
+ assert_image_equal_tofile,
+ assert_image_similar,
+ hopper,
+)
# sample ppm stream
TEST_FILE = "Tests/images/hopper.ppm"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(TEST_FILE) as im:
assert im.mode == "RGB"
assert im.size == (128, 128)
@@ -62,7 +70,7 @@ def test_sanity():
),
),
)
-def test_arbitrary_maxval(data, mode, pixels):
+def test_arbitrary_maxval(data, mode, pixels) -> None:
fp = BytesIO(data)
with Image.open(fp) as im:
assert im.size == (3, 1)
@@ -72,7 +80,7 @@ def test_arbitrary_maxval(data, mode, pixels):
assert tuple(px[x, 0] for x in range(3)) == pixels
-def test_16bit_pgm():
+def test_16bit_pgm() -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im:
assert im.mode == "I"
assert im.size == (20, 100)
@@ -81,22 +89,60 @@ def test_16bit_pgm():
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png")
-def test_16bit_pgm_write(tmp_path):
+def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im:
- f = str(tmp_path / "temp.pgm")
- im.save(f, "PPM")
+ filename = str(tmp_path / "temp.pgm")
+ im.save(filename, "PPM")
- assert_image_equal_tofile(im, f)
+ assert_image_equal_tofile(im, filename)
-def test_pnm(tmp_path):
+def test_pnm(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.pnm") as im:
assert_image_similar(im, hopper(), 0.0001)
- f = str(tmp_path / "temp.pnm")
- im.save(f)
+ filename = str(tmp_path / "temp.pnm")
+ im.save(filename)
- assert_image_equal_tofile(im, f)
+ assert_image_equal_tofile(im, filename)
+
+
+def test_pfm(tmp_path: Path) -> None:
+ with Image.open("Tests/images/hopper.pfm") as im:
+ assert im.info["scale"] == 1.0
+ assert_image_equal(im, hopper("F"))
+
+ filename = str(tmp_path / "tmp.pfm")
+ im.save(filename)
+
+ assert_image_equal_tofile(im, filename)
+
+
+def test_pfm_big_endian(tmp_path: Path) -> None:
+ with Image.open("Tests/images/hopper_be.pfm") as im:
+ assert im.info["scale"] == 2.5
+ assert_image_equal(im, hopper("F"))
+
+ filename = str(tmp_path / "tmp.pfm")
+ im.save(filename)
+
+ assert_image_equal_tofile(im, filename)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ b"Pf 1 1 NaN \0\0\0\0",
+ b"Pf 1 1 inf \0\0\0\0",
+ b"Pf 1 1 -inf \0\0\0\0",
+ b"Pf 1 1 0.0 \0\0\0\0",
+ b"Pf 1 1 -0.0 \0\0\0\0",
+ ],
+)
+def test_pfm_invalid(data) -> None:
+ with pytest.raises(ValueError):
+ with Image.open(BytesIO(data)):
+ pass
@pytest.mark.parametrize(
@@ -116,12 +162,12 @@ def test_pnm(tmp_path):
),
),
)
-def test_plain(plain_path, raw_path):
+def test_plain(plain_path, raw_path) -> None:
with Image.open(plain_path) as im:
assert_image_equal_tofile(im, raw_path)
-def test_16bit_plain_pgm():
+def test_16bit_plain_pgm() -> None:
# P2 with maxval 2 ** 16 - 1
with Image.open("Tests/images/hopper_16bit_plain.pgm") as im:
assert im.mode == "I"
@@ -140,7 +186,7 @@ def test_16bit_plain_pgm():
(b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6),
),
)
-def test_plain_data_with_comment(tmp_path, header, data, comment_count):
+def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> None:
path1 = str(tmp_path / "temp1.ppm")
path2 = str(tmp_path / "temp2.ppm")
comment = b"# comment" * comment_count
@@ -153,7 +199,7 @@ def test_plain_data_with_comment(tmp_path, header, data, comment_count):
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
-def test_plain_truncated_data(tmp_path, data):
+def test_plain_truncated_data(tmp_path: Path, data) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(data)
@@ -164,7 +210,7 @@ def test_plain_truncated_data(tmp_path, data):
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
-def test_plain_invalid_data(tmp_path, data):
+def test_plain_invalid_data(tmp_path: Path, data) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(data)
@@ -181,7 +227,7 @@ def test_plain_invalid_data(tmp_path, data):
b"P3\n128 128\n255\n012345678910 0", # token too long
),
)
-def test_plain_ppm_token_too_long(tmp_path, data):
+def test_plain_ppm_token_too_long(tmp_path: Path, data) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(data)
@@ -191,7 +237,7 @@ def test_plain_ppm_token_too_long(tmp_path, data):
im.load()
-def test_plain_ppm_value_too_large(tmp_path):
+def test_plain_ppm_value_too_large(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n256")
@@ -201,12 +247,12 @@ def test_plain_ppm_value_too_large(tmp_path):
im.load()
-def test_magic():
+def test_magic() -> None:
with pytest.raises(SyntaxError):
PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid"))
-def test_header_with_comments(tmp_path):
+def test_header_with_comments(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n")
@@ -215,7 +261,7 @@ def test_header_with_comments(tmp_path):
assert im.size == (128, 128)
-def test_non_integer_token(tmp_path):
+def test_non_integer_token(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\nTEST")
@@ -225,7 +271,7 @@ def test_non_integer_token(tmp_path):
pass
-def test_header_token_too_long(tmp_path):
+def test_header_token_too_long(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n 01234567890")
@@ -237,7 +283,7 @@ def test_header_token_too_long(tmp_path):
assert str(e.value) == "Token too long in file header: 01234567890"
-def test_truncated_file(tmp_path):
+def test_truncated_file(tmp_path: Path) -> None:
# Test EOF in header
path = str(tmp_path / "temp.pgm")
with open(path, "wb") as f:
@@ -256,7 +302,7 @@ def test_truncated_file(tmp_path):
im.load()
-def test_not_enough_image_data(tmp_path):
+def test_not_enough_image_data(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P2 1 2 255 255")
@@ -267,7 +313,7 @@ def test_not_enough_image_data(tmp_path):
@pytest.mark.parametrize("maxval", (b"0", b"65536"))
-def test_invalid_maxval(maxval, tmp_path):
+def test_invalid_maxval(maxval, tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n3 1 " + maxval)
@@ -279,7 +325,7 @@ def test_invalid_maxval(maxval, tmp_path):
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
-def test_neg_ppm():
+def test_neg_ppm() -> None:
# Storage.c accepted negative values for xsize, ysize. the
# internal open_ppm function didn't check for sanity but it
# has been removed. The default opener doesn't accept negative
@@ -290,7 +336,7 @@ def test_neg_ppm():
pass
-def test_mimetypes(tmp_path):
+def test_mimetypes(tmp_path: Path) -> None:
path = str(tmp_path / "temp.pgm")
with open(path, "wb") as f:
@@ -305,7 +351,7 @@ def test_mimetypes(tmp_path):
@pytest.mark.parametrize("buffer", (True, False))
-def test_save_stdout(buffer):
+def test_save_stdout(buffer) -> None:
old_stdout = sys.stdout
if buffer:
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index e405834b5..7eca8d9b1 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import warnings
import pytest
@@ -9,7 +11,7 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_
test_file = "Tests/images/hopper.psd"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(test_file) as im:
im.load()
assert im.mode == "RGB"
@@ -22,8 +24,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
-def test_unclosed_file():
- def open():
+def test_unclosed_file() -> None:
+ def open() -> None:
im = Image.open(test_file)
im.load()
@@ -31,27 +33,27 @@ def test_unclosed_file():
open()
-def test_closed_file():
+def test_closed_file() -> None:
with warnings.catch_warnings():
im = Image.open(test_file)
im.load()
im.close()
-def test_context_manager():
+def test_context_manager() -> None:
with warnings.catch_warnings():
with Image.open(test_file) as im:
im.load()
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
PsdImagePlugin.PsdImageFile(invalid_file)
-def test_n_frames():
+def test_n_frames() -> None:
with Image.open("Tests/images/hopper_merged.psd") as im:
assert im.n_frames == 1
assert not im.is_animated
@@ -62,7 +64,7 @@ def test_n_frames():
assert im.is_animated
-def test_eoferror():
+def test_eoferror() -> None:
with Image.open(test_file) as im:
# PSD seek index starts at 1 rather than 0
n_frames = im.n_frames + 1
@@ -76,7 +78,7 @@ def test_eoferror():
im.seek(n_frames - 1)
-def test_seek_tell():
+def test_seek_tell() -> None:
with Image.open(test_file) as im:
layer_number = im.tell()
assert layer_number == 1
@@ -93,25 +95,30 @@ def test_seek_tell():
assert layer_number == 2
-def test_seek_eoferror():
+def test_seek_eoferror() -> None:
with Image.open(test_file) as im:
with pytest.raises(EOFError):
im.seek(-1)
-def test_open_after_exclusive_load():
+def test_open_after_exclusive_load() -> None:
with Image.open(test_file) as im:
im.load()
im.seek(im.tell() + 1)
im.load()
-def test_rgba():
+def test_rgba() -> None:
with Image.open("Tests/images/rgba.psd") as im:
assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
-def test_icc_profile():
+def test_layer_skip() -> None:
+ with Image.open("Tests/images/five_channels.psd") as im:
+ assert im.n_frames == 1
+
+
+def test_icc_profile() -> None:
with Image.open(test_file) as im:
assert "icc_profile" in im.info
@@ -119,12 +126,12 @@ def test_icc_profile():
assert len(icc_profile) == 3144
-def test_no_icc_profile():
+def test_no_icc_profile() -> None:
with Image.open("Tests/images/hopper_merged.psd") as im:
assert "icc_profile" not in im.info
-def test_combined_larger_than_size():
+def test_combined_larger_than_size() -> None:
# The combined size of the individual parts is larger than the
# declared 'size' of the extra data field, resulting in a backwards seek.
@@ -150,7 +157,7 @@ def test_combined_larger_than_size():
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
],
)
-def test_crashes(test_file, raises):
+def test_crashes(test_file, raises) -> None:
with open(test_file, "rb") as f:
with pytest.raises(raises):
with Image.open(f):
diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py
index 0a835dcf6..fd4b981ce 100644
--- a/Tests/test_file_qoi.py
+++ b/Tests/test_file_qoi.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, QoiImagePlugin
@@ -5,7 +7,7 @@ from PIL import Image, QoiImagePlugin
from .helper import assert_image_equal_tofile
-def test_sanity():
+def test_sanity() -> None:
with Image.open("Tests/images/hopper.qoi") as im:
assert im.mode == "RGB"
assert im.size == (128, 128)
@@ -21,7 +23,7 @@ def test_sanity():
assert_image_equal_tofile(im, "Tests/images/pil123rgba.png")
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py
index 6a5d8887d..92aea0735 100644
--- a/Tests/test_file_sgi.py
+++ b/Tests/test_file_sgi.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, SgiImagePlugin
@@ -10,7 +14,7 @@ from .helper import (
)
-def test_rgb():
+def test_rgb() -> None:
# Created with ImageMagick then renamed:
# convert hopper.ppm -compress None sgi:hopper.rgb
test_file = "Tests/images/hopper.rgb"
@@ -20,11 +24,11 @@ def test_rgb():
assert im.get_format_mimetype() == "image/rgb"
-def test_rgb16():
+def test_rgb16() -> None:
assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb")
-def test_l():
+def test_l() -> None:
# Created with ImageMagick
# convert hopper.ppm -monochrome -compress None sgi:hopper.bw
test_file = "Tests/images/hopper.bw"
@@ -34,7 +38,7 @@ def test_l():
assert im.get_format_mimetype() == "image/sgi"
-def test_rgba():
+def test_rgba() -> None:
# Created with ImageMagick:
# convert transparent.png -compress None transparent.sgi
test_file = "Tests/images/transparent.sgi"
@@ -44,7 +48,7 @@ def test_rgba():
assert im.get_format_mimetype() == "image/sgi"
-def test_rle():
+def test_rle() -> None:
# Created with ImageMagick:
# convert hopper.ppm hopper.sgi
test_file = "Tests/images/hopper.sgi"
@@ -53,22 +57,22 @@ def test_rle():
assert_image_equal_tofile(im, "Tests/images/hopper.rgb")
-def test_rle16():
+def test_rle16() -> None:
test_file = "Tests/images/tv16.sgi"
with Image.open(test_file) as im:
assert_image_equal_tofile(im, "Tests/images/tv.rgb")
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(ValueError):
SgiImagePlugin.SgiImageFile(invalid_file)
-def test_write(tmp_path):
- def roundtrip(img):
+def test_write(tmp_path: Path) -> None:
+ def roundtrip(img) -> None:
out = str(tmp_path / "temp.sgi")
img.save(out, format="sgi")
assert_image_equal_tofile(img, out)
@@ -87,7 +91,7 @@ def test_write(tmp_path):
roundtrip(Image.new("L", (10, 1)))
-def test_write16(tmp_path):
+def test_write16(tmp_path: Path) -> None:
test_file = "Tests/images/hopper16.rgb"
with Image.open(test_file) as im:
@@ -97,7 +101,7 @@ def test_write16(tmp_path):
assert_image_equal_tofile(im, out)
-def test_unsupported_mode(tmp_path):
+def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("LA")
out = str(tmp_path / "temp.sgi")
diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py
index 09f1ef8e4..75fef1dc6 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -1,6 +1,9 @@
+from __future__ import annotations
+
import tempfile
import warnings
from io import BytesIO
+from pathlib import Path
import pytest
@@ -11,7 +14,7 @@ from .helper import assert_image_equal_tofile, hopper, is_pypy
TEST_FILE = "Tests/images/hopper.spider"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(TEST_FILE) as im:
im.load()
assert im.mode == "F"
@@ -20,8 +23,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
-def test_unclosed_file():
- def open():
+def test_unclosed_file() -> None:
+ def open() -> None:
im = Image.open(TEST_FILE)
im.load()
@@ -29,20 +32,20 @@ def test_unclosed_file():
open()
-def test_closed_file():
+def test_closed_file() -> None:
with warnings.catch_warnings():
im = Image.open(TEST_FILE)
im.load()
im.close()
-def test_context_manager():
+def test_context_manager() -> None:
with warnings.catch_warnings():
with Image.open(TEST_FILE) as im:
im.load()
-def test_save(tmp_path):
+def test_save(tmp_path: Path) -> None:
# Arrange
temp = str(tmp_path / "temp.spider")
im = hopper()
@@ -57,7 +60,7 @@ def test_save(tmp_path):
assert im2.format == "SPIDER"
-def test_tempfile():
+def test_tempfile() -> None:
# Arrange
im = hopper()
@@ -73,11 +76,11 @@ def test_tempfile():
assert reloaded.format == "SPIDER"
-def test_is_spider_image():
+def test_is_spider_image() -> None:
assert SpiderImagePlugin.isSpiderImage(TEST_FILE)
-def test_tell():
+def test_tell() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
# Act
@@ -87,13 +90,13 @@ def test_tell():
assert index == 0
-def test_n_frames():
+def test_n_frames() -> None:
with Image.open(TEST_FILE) as im:
assert im.n_frames == 1
assert not im.is_animated
-def test_load_image_series():
+def test_load_image_series() -> None:
# Arrange
not_spider_file = "Tests/images/hopper.ppm"
file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"]
@@ -107,7 +110,7 @@ def test_load_image_series():
assert img_list[0].size == (128, 128)
-def test_load_image_series_no_input():
+def test_load_image_series_no_input() -> None:
# Arrange
file_list = None
@@ -118,7 +121,7 @@ def test_load_image_series_no_input():
assert img_list is None
-def test_is_int_not_a_number():
+def test_is_int_not_a_number() -> None:
# Arrange
not_a_number = "a"
@@ -129,7 +132,7 @@ def test_is_int_not_a_number():
assert ret == 0
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/invalid.spider"
with pytest.raises(OSError):
@@ -137,20 +140,20 @@ def test_invalid_file():
pass
-def test_nonstack_file():
+def test_nonstack_file() -> None:
with Image.open(TEST_FILE) as im:
with pytest.raises(EOFError):
im.seek(0)
-def test_nonstack_dos():
+def test_nonstack_dos() -> None:
with Image.open(TEST_FILE) as im:
for i, frame in enumerate(ImageSequence.Iterator(im)):
assert i <= 1, "Non-stack DOS file test failed"
# for issue #4093
-def test_odd_size():
+def test_odd_size() -> None:
data = BytesIO()
width = 100
im = Image.new("F", (width, 64))
diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py
index edb320603..6cfff8730 100644
--- a/Tests/test_file_sun.py
+++ b/Tests/test_file_sun.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import os
import pytest
@@ -9,7 +11,7 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper
EXTRA_DIR = "Tests/images/sunraster"
-def test_sanity():
+def test_sanity() -> None:
# Arrange
# Created with ImageMagick: convert hopper.jpg hopper.ras
test_file = "Tests/images/hopper.ras"
@@ -26,7 +28,7 @@ def test_sanity():
SunImagePlugin.SunImageFile(invalid_file)
-def test_im1():
+def test_im1() -> None:
with Image.open("Tests/images/sunraster.im1") as im:
assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png")
@@ -34,7 +36,7 @@ def test_im1():
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
-def test_others():
+def test_others() -> None:
files = (
os.path.join(EXTRA_DIR, f)
for f in os.listdir(EXTRA_DIR)
diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py
index b27fa25f3..44e78e972 100644
--- a/Tests/test_file_tar.py
+++ b/Tests/test_file_tar.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import warnings
import pytest
@@ -17,7 +19,7 @@ TEST_TAR_FILE = "Tests/images/hopper.tar"
("jpg", "hopper.jpg", "JPEG"),
),
)
-def test_sanity(codec, test_path, format):
+def test_sanity(codec, test_path, format) -> None:
if features.check(codec):
with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar:
with Image.open(tar) as im:
@@ -28,18 +30,18 @@ def test_sanity(codec, test_path, format):
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
-def test_unclosed_file():
+def test_unclosed_file() -> None:
with pytest.warns(ResourceWarning):
TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
-def test_close():
+def test_close() -> None:
with warnings.catch_warnings():
tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
tar.close()
-def test_contextmanager():
+def test_contextmanager() -> None:
with warnings.catch_warnings():
with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"):
pass
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index 1a5730f49..bd8e522c7 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -1,6 +1,9 @@
+from __future__ import annotations
+
import os
from glob import glob
from itertools import product
+from pathlib import Path
import pytest
@@ -19,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
@pytest.mark.parametrize("mode", _MODES)
-def test_sanity(mode, tmp_path):
- def roundtrip(original_im):
+def test_sanity(mode, tmp_path: Path) -> None:
+ def roundtrip(original_im) -> None:
out = str(tmp_path / "temp.tga")
original_im.save(out, rle=rle)
@@ -62,7 +65,7 @@ def test_sanity(mode, tmp_path):
roundtrip(original_im)
-def test_palette_depth_16(tmp_path):
+def test_palette_depth_16(tmp_path: Path) -> None:
with Image.open("Tests/images/p_16.tga") as im:
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png")
@@ -72,7 +75,7 @@ def test_palette_depth_16(tmp_path):
assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png")
-def test_id_field():
+def test_id_field() -> None:
# tga file with id field
test_file = "Tests/images/tga_id_field.tga"
@@ -82,7 +85,7 @@ def test_id_field():
assert im.size == (100, 100)
-def test_id_field_rle():
+def test_id_field_rle() -> None:
# tga file with id field
test_file = "Tests/images/rgb32rle.tga"
@@ -92,7 +95,7 @@ def test_id_field_rle():
assert im.size == (199, 199)
-def test_cross_scan_line():
+def test_cross_scan_line() -> None:
with Image.open("Tests/images/cross_scan_line.tga") as im:
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
@@ -101,7 +104,7 @@ def test_cross_scan_line():
im.load()
-def test_save(tmp_path):
+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")
@@ -118,7 +121,7 @@ def test_save(tmp_path):
assert test_im.size == (100, 100)
-def test_small_palette(tmp_path):
+def test_small_palette(tmp_path: Path) -> None:
im = Image.new("P", (1, 1))
colors = [0, 0, 0]
im.putpalette(colors)
@@ -130,7 +133,7 @@ def test_small_palette(tmp_path):
assert reloaded.getpalette() == colors
-def test_save_wrong_mode(tmp_path):
+def test_save_wrong_mode(tmp_path: Path) -> None:
im = hopper("PA")
out = str(tmp_path / "temp.tga")
@@ -138,7 +141,7 @@ def test_save_wrong_mode(tmp_path):
im.save(out)
-def test_save_mapdepth():
+def test_save_mapdepth() -> None:
# This image has been manually hexedited from 200x32_p_bl_raw.tga
# to include an origin
test_file = "Tests/images/200x32_p_bl_raw_origin.tga"
@@ -146,7 +149,7 @@ def test_save_mapdepth():
assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png")
-def test_save_id_section(tmp_path):
+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")
@@ -177,7 +180,7 @@ def test_save_id_section(tmp_path):
assert "id_section" not in test_im.info
-def test_save_orientation(tmp_path):
+def test_save_orientation(tmp_path: Path) -> None:
test_file = "Tests/images/rgb32rle.tga"
out = str(tmp_path / "temp.tga")
with Image.open(test_file) as im:
@@ -188,7 +191,7 @@ def test_save_orientation(tmp_path):
assert test_im.info["orientation"] == 1
-def test_horizontal_orientations():
+def test_horizontal_orientations() -> None:
# These images have been manually hexedited to have the relevant orientations
with Image.open("Tests/images/rgb32rle_top_right.tga") as im:
assert im.load()[90, 90][:3] == (0, 0, 0)
@@ -197,7 +200,7 @@ def test_horizontal_orientations():
assert im.load()[90, 90][:3] == (0, 255, 0)
-def test_save_rle(tmp_path):
+def test_save_rle(tmp_path: Path) -> None:
test_file = "Tests/images/rgb32rle.tga"
with Image.open(test_file) as im:
assert im.info["compression"] == "tga_rle"
@@ -230,7 +233,7 @@ def test_save_rle(tmp_path):
assert test_im.info["compression"] == "tga_rle"
-def test_save_l_transparency(tmp_path):
+def test_save_l_transparency(tmp_path: Path) -> None:
# There are 559 transparent pixels in la.tga.
num_transparent = 559
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 7362c93ca..a16b76e19 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -1,6 +1,9 @@
+from __future__ import annotations
+
import os
import warnings
from io import BytesIO
+from pathlib import Path
import pytest
@@ -24,7 +27,7 @@ except ImportError:
class TestFileTiff:
- def test_sanity(self, tmp_path):
+ def test_sanity(self, tmp_path: Path) -> None:
filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename)
@@ -56,21 +59,21 @@ class TestFileTiff:
pass
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
- def test_unclosed_file(self):
- def open():
+ def test_unclosed_file(self) -> None:
+ def open() -> None:
im = Image.open("Tests/images/multipage.tiff")
im.load()
with pytest.warns(ResourceWarning):
open()
- def test_closed_file(self):
+ def test_closed_file(self) -> None:
with warnings.catch_warnings():
im = Image.open("Tests/images/multipage.tiff")
im.load()
im.close()
- def test_seek_after_close(self):
+ def test_seek_after_close(self) -> None:
im = Image.open("Tests/images/multipage.tiff")
im.close()
@@ -79,12 +82,12 @@ class TestFileTiff:
with pytest.raises(ValueError):
im.seek(1)
- def test_context_manager(self):
+ def test_context_manager(self) -> None:
with warnings.catch_warnings():
with Image.open("Tests/images/multipage.tiff") as im:
im.load()
- def test_mac_tiff(self):
+ def test_mac_tiff(self) -> None:
# Read RGBa images from macOS [@PIL136]
filename = "Tests/images/pil136.tiff"
@@ -96,7 +99,7 @@ class TestFileTiff:
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
- def test_bigtiff(self, tmp_path):
+ def test_bigtiff(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
@@ -107,13 +110,13 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
- def test_set_legacy_api(self):
+ def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e:
ifd.legacy_api = None
assert str(e.value) == "Not allowing setting of legacy api"
- def test_xyres_tiff(self):
+ def test_xyres_tiff(self) -> None:
filename = "Tests/images/pil168.tif"
with Image.open(filename) as im:
# legacy api
@@ -126,7 +129,7 @@ class TestFileTiff:
assert im.info["dpi"] == (72.0, 72.0)
- def test_xyres_fallback_tiff(self):
+ def test_xyres_fallback_tiff(self) -> None:
filename = "Tests/images/compression.tif"
with Image.open(filename) as im:
# v2 api
@@ -140,7 +143,7 @@ class TestFileTiff:
# Fallback "inch".
assert im.info["dpi"] == (100.0, 100.0)
- def test_int_resolution(self):
+ def test_int_resolution(self) -> None:
filename = "Tests/images/pil168.tif"
with Image.open(filename) as im:
# Try to read a file where X,Y_RESOLUTION are ints
@@ -153,14 +156,14 @@ class TestFileTiff:
"resolution_unit, dpi",
[(None, 72.8), (2, 72.8), (3, 184.912)],
)
- def test_load_float_dpi(self, resolution_unit, dpi):
+ def test_load_float_dpi(self, resolution_unit, dpi) -> None:
with Image.open(
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
) as im:
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
assert im.info["dpi"] == (dpi, dpi)
- def test_save_float_dpi(self, tmp_path):
+ def test_save_float_dpi(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/hopper.tif") as im:
dpi = (72.2, 72.2)
@@ -169,7 +172,7 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert reloaded.info["dpi"] == dpi
- def test_save_setting_missing_resolution(self):
+ def test_save_setting_missing_resolution(self) -> None:
b = BytesIO()
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
im.save(b, format="tiff", resolution=123.45)
@@ -177,7 +180,7 @@ class TestFileTiff:
assert im.tag_v2[X_RESOLUTION] == 123.45
assert im.tag_v2[Y_RESOLUTION] == 123.45
- def test_invalid_file(self):
+ def test_invalid_file(self) -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
@@ -188,30 +191,30 @@ class TestFileTiff:
TiffImagePlugin.TiffImageFile(invalid_file)
TiffImagePlugin.PREFIXES.pop()
- def test_bad_exif(self):
+ def test_bad_exif(self) -> None:
with Image.open("Tests/images/hopper_bad_exif.jpg") as i:
# Should not raise struct.error.
with pytest.warns(UserWarning):
i._getexif()
- def test_save_rgba(self, tmp_path):
+ def test_save_rgba(self, tmp_path: Path) -> None:
im = hopper("RGBA")
outfile = str(tmp_path / "temp.tif")
im.save(outfile)
- def test_save_unsupported_mode(self, tmp_path):
+ def test_save_unsupported_mode(self, tmp_path: Path) -> None:
im = hopper("HSV")
outfile = str(tmp_path / "temp.tif")
with pytest.raises(OSError):
im.save(outfile)
- def test_8bit_s(self):
+ def test_8bit_s(self) -> None:
with Image.open("Tests/images/8bit.s.tif") as im:
im.load()
assert im.mode == "L"
assert im.getpixel((50, 50)) == 184
- def test_little_endian(self):
+ def test_little_endian(self) -> None:
with Image.open("Tests/images/16bit.cropped.tif") as im:
assert im.getpixel((0, 0)) == 480
assert im.mode == "I;16"
@@ -221,7 +224,7 @@ class TestFileTiff:
assert b[0] == ord(b"\xe0")
assert b[1] == ord(b"\x01")
- def test_big_endian(self):
+ def test_big_endian(self) -> None:
with Image.open("Tests/images/16bit.MM.cropped.tif") as im:
assert im.getpixel((0, 0)) == 480
assert im.mode == "I;16B"
@@ -231,7 +234,7 @@ class TestFileTiff:
assert b[0] == ord(b"\x01")
assert b[1] == ord(b"\xe0")
- def test_16bit_r(self):
+ def test_16bit_r(self) -> None:
with Image.open("Tests/images/16bit.r.tif") as im:
assert im.getpixel((0, 0)) == 480
assert im.mode == "I;16"
@@ -240,14 +243,14 @@ class TestFileTiff:
assert b[0] == ord(b"\xe0")
assert b[1] == ord(b"\x01")
- def test_16bit_s(self):
+ def test_16bit_s(self) -> None:
with Image.open("Tests/images/16bit.s.tif") as im:
im.load()
assert im.mode == "I"
assert im.getpixel((0, 0)) == 32767
assert im.getpixel((0, 1)) == 0
- def test_12bit_rawmode(self):
+ def test_12bit_rawmode(self) -> None:
"""Are we generating the same interpretation
of the image as Imagemagick is?"""
@@ -260,7 +263,7 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/12in16bit.tif")
- def test_32bit_float(self):
+ def test_32bit_float(self) -> None:
# Issue 614, specific 32-bit float format
path = "Tests/images/10ct_32bit_128.tiff"
with Image.open(path) as im:
@@ -269,7 +272,7 @@ class TestFileTiff:
assert im.getpixel((0, 0)) == -0.4526388943195343
assert im.getextrema() == (-3.140936851501465, 3.140684127807617)
- def test_unknown_pixel_mode(self):
+ def test_unknown_pixel_mode(self) -> None:
with pytest.raises(OSError):
with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"):
pass
@@ -281,12 +284,12 @@ class TestFileTiff:
("Tests/images/multipage.tiff", 3),
),
)
- def test_n_frames(self, path, n_frames):
+ def test_n_frames(self, path, n_frames) -> None:
with Image.open(path) as im:
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
- def test_eoferror(self):
+ def test_eoferror(self) -> None:
with Image.open("Tests/images/multipage-lastframe.tif") as im:
n_frames = im.n_frames
@@ -298,7 +301,7 @@ class TestFileTiff:
# Test that seeking to the last frame does not raise an error
im.seek(n_frames - 1)
- def test_multipage(self):
+ def test_multipage(self) -> None:
# issue #862
with Image.open("Tests/images/multipage.tiff") as im:
# file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue
@@ -322,13 +325,13 @@ class TestFileTiff:
assert im.size == (20, 20)
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
- def test_multipage_last_frame(self):
+ def test_multipage_last_frame(self) -> None:
with Image.open("Tests/images/multipage-lastframe.tif") as im:
im.load()
assert im.size == (20, 20)
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
- def test_frame_order(self):
+ 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 im.n_frames == 1
@@ -341,7 +344,7 @@ class TestFileTiff:
with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
assert im.n_frames == 3
- def test___str__(self):
+ def test___str__(self) -> None:
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
# Act
@@ -350,7 +353,7 @@ class TestFileTiff:
# Assert
assert isinstance(ret, str)
- def test_dict(self):
+ def test_dict(self) -> None:
# Arrange
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
@@ -390,7 +393,7 @@ class TestFileTiff:
}
assert dict(im.tag) == legacy_tags
- def test__delitem__(self):
+ def test__delitem__(self) -> None:
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
len_before = len(dict(im.ifd))
@@ -399,36 +402,36 @@ class TestFileTiff:
assert len_before == len_after + 1
@pytest.mark.parametrize("legacy_api", (False, True))
- def test_load_byte(self, legacy_api):
+ def test_load_byte(self, legacy_api) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abc"
ret = ifd.load_byte(data, legacy_api)
assert ret == b"abc"
- def test_load_string(self):
+ def test_load_string(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abc\0"
ret = ifd.load_string(data, False)
assert ret == "abc"
- def test_load_float(self):
+ def test_load_float(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdabcd"
ret = ifd.load_float(data, False)
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
- def test_load_double(self):
+ def test_load_double(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdefghabcdefgh"
ret = ifd.load_double(data, False)
assert ret == (8.540883223036124e194, 8.540883223036124e194)
- def test_ifd_tag_type(self):
+ def test_ifd_tag_type(self) -> None:
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
assert 0x8825 in im.tag_v2
- def test_exif(self, tmp_path):
- def check_exif(exif):
+ def test_exif(self, tmp_path: Path) -> None:
+ def check_exif(exif) -> None:
assert sorted(exif.keys()) == [
256,
257,
@@ -479,19 +482,19 @@ class TestFileTiff:
exif = im.getexif()
check_exif(exif)
- def test_modify_exif(self, tmp_path):
+ def test_modify_exif(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
- exif[256] = 100
+ exif[264] = 100
im.save(outfile, exif=exif)
with Image.open(outfile) as im:
exif = im.getexif()
- assert exif[256] == 100
+ assert exif[264] == 100
- def test_reload_exif_after_seek(self):
+ def test_reload_exif_after_seek(self) -> None:
with Image.open("Tests/images/multipage.tiff") as im:
exif = im.getexif()
del exif[256]
@@ -499,7 +502,7 @@ class TestFileTiff:
assert 256 in exif
- def test_exif_frames(self):
+ def test_exif_frames(self) -> None:
# Test that EXIF data can change across frames
with Image.open("Tests/images/g4-multi.tiff") as im:
assert im.getexif()[273] == (328, 815)
@@ -508,7 +511,7 @@ class TestFileTiff:
assert im.getexif()[273] == (1408, 1907)
@pytest.mark.parametrize("mode", ("1", "L"))
- def test_photometric(self, mode, tmp_path):
+ def test_photometric(self, mode, tmp_path: Path) -> None:
filename = str(tmp_path / "temp.tif")
im = hopper(mode)
im.save(filename, tiffinfo={262: 0})
@@ -516,13 +519,13 @@ class TestFileTiff:
assert reloaded.tag_v2[262] == 0
assert_image_equal(im, reloaded)
- def test_seek(self):
+ def test_seek(self) -> None:
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
im.seek(0)
assert im.tell() == 0
- def test_seek_eof(self):
+ def test_seek_eof(self) -> None:
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
assert im.tell() == 0
@@ -531,21 +534,21 @@ class TestFileTiff:
with pytest.raises(EOFError):
im.seek(1)
- def test__limit_rational_int(self):
+ def test__limit_rational_int(self) -> None:
from PIL.TiffImagePlugin import _limit_rational
value = 34
ret = _limit_rational(value, 65536)
assert ret == (34, 1)
- def test__limit_rational_float(self):
+ def test__limit_rational_float(self) -> None:
from PIL.TiffImagePlugin import _limit_rational
value = 22.3
ret = _limit_rational(value, 65536)
assert ret == (223, 10)
- def test_4bit(self):
+ def test_4bit(self) -> None:
test_file = "Tests/images/hopper_gray_4bpp.tif"
original = hopper("L")
with Image.open(test_file) as im:
@@ -553,7 +556,7 @@ class TestFileTiff:
assert im.mode == "L"
assert_image_similar(im, original, 7.3)
- def test_gray_semibyte_per_pixel(self):
+ def test_gray_semibyte_per_pixel(self) -> None:
test_files = (
(
24.8, # epsilon
@@ -586,7 +589,7 @@ class TestFileTiff:
assert im2.mode == "L"
assert_image_equal(im, im2)
- def test_with_underscores(self, tmp_path):
+ 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")
hopper("RGB").save(filename, **kwargs)
@@ -599,7 +602,7 @@ class TestFileTiff:
assert im.tag_v2[X_RESOLUTION] == 72
assert im.tag_v2[Y_RESOLUTION] == 36
- def test_roundtrip_tiff_uint16(self, tmp_path):
+ def test_roundtrip_tiff_uint16(self, tmp_path: Path) -> None:
# Test an image of all '0' values
pixel_value = 0x1234
infile = "Tests/images/uint16_1_4660.tif"
@@ -611,25 +614,33 @@ class TestFileTiff:
assert_image_equal_tofile(im, tmpfile)
- def test_strip_raw(self):
+ def test_rowsperstrip(self, tmp_path: Path) -> None:
+ outfile = str(tmp_path / "temp.tif")
+ im = hopper()
+ im.save(outfile, tiffinfo={278: 256})
+
+ with Image.open(outfile) as im:
+ 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:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
- def test_strip_planar_raw(self):
+ def test_strip_planar_raw(self) -> None:
# gdal_translate -of GTiff -co INTERLEAVE=BAND \
# tiff_strip_raw.tif tiff_strip_planar_raw.tiff
infile = "Tests/images/tiff_strip_planar_raw.tif"
with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
- def test_strip_planar_raw_with_overviews(self):
+ def test_strip_planar_raw_with_overviews(self) -> None:
# gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16
infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif"
with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
- def test_tiled_planar_raw(self):
+ def test_tiled_planar_raw(self) -> None:
# gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \
# -co BLOCKYSIZE=32 -co INTERLEAVE=BAND \
# tiff_tiled_raw.tif tiff_tiled_planar_raw.tiff
@@ -637,7 +648,7 @@ class TestFileTiff:
with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
- def test_planar_configuration_save(self, tmp_path):
+ 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 im._planar_configuration == 2
@@ -649,7 +660,7 @@ class TestFileTiff:
assert_image_equal_tofile(reloaded, infile)
@pytest.mark.parametrize("mode", ("P", "PA"))
- def test_palette(self, mode, tmp_path):
+ def test_palette(self, mode, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
im = hopper(mode)
@@ -658,7 +669,7 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
- def test_tiff_save_all(self):
+ def test_tiff_save_all(self) -> None:
mp = BytesIO()
with Image.open("Tests/images/multipage.tiff") as im:
im.save(mp, format="tiff", save_all=True)
@@ -688,7 +699,7 @@ class TestFileTiff:
with Image.open(mp) as reread:
assert reread.n_frames == 3
- def test_saving_icc_profile(self, tmp_path):
+ def test_saving_icc_profile(self, tmp_path: Path) -> None:
# Tests saving TIFF with icc_profile set.
# At the time of writing this will only work for non-compressed tiffs
# as libtiff does not support embedded ICC profiles,
@@ -702,7 +713,7 @@ class TestFileTiff:
with Image.open(tmpfile) as reloaded:
assert b"Dummy value" == reloaded.info["icc_profile"]
- def test_save_icc_profile(self, tmp_path):
+ def test_save_icc_profile(self, tmp_path: Path) -> None:
im = hopper()
assert "icc_profile" not in im.info
@@ -713,14 +724,14 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert reloaded.info["icc_profile"] == icc_profile
- def test_save_bmp_compression(self, tmp_path):
+ def test_save_bmp_compression(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["compression"] == 0
outfile = str(tmp_path / "temp.tif")
im.save(outfile)
- def test_discard_icc_profile(self, tmp_path):
+ def test_discard_icc_profile(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/icc_profile.png") as im:
@@ -731,7 +742,7 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert "icc_profile" not in reloaded.info
- def test_getxmp(self):
+ def test_getxmp(self) -> None:
with Image.open("Tests/images/lab.tif") as im:
if ElementTree is None:
with pytest.warns(
@@ -746,7 +757,7 @@ class TestFileTiff:
assert description[0]["format"] == "image/tiff"
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
- def test_get_photoshop_blocks(self):
+ def test_get_photoshop_blocks(self) -> None:
with Image.open("Tests/images/lab.tif") as im:
assert list(im.get_photoshop_blocks().keys()) == [
1061,
@@ -772,7 +783,28 @@ class TestFileTiff:
4001,
]
- def test_close_on_load_exclusive(self, tmp_path):
+ def test_tiff_chunks(self, tmp_path: Path) -> None:
+ tmpfile = str(tmp_path / "temp.tif")
+
+ im = hopper()
+ with open(tmpfile, "wb") as fp:
+ for y in range(0, 128, 32):
+ chunk = im.crop((0, y, 128, y + 32))
+ if y == 0:
+ chunk.save(
+ fp,
+ "TIFF",
+ tiffinfo={
+ TiffImagePlugin.IMAGEWIDTH: 128,
+ TiffImagePlugin.IMAGELENGTH: 128,
+ },
+ )
+ else:
+ fp.write(chunk.tobytes())
+
+ assert_image_equal_tofile(im, tmpfile)
+
+ 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")
@@ -785,7 +817,7 @@ class TestFileTiff:
im.load()
assert fp.closed
- def test_close_on_load_nonexclusive(self, tmp_path):
+ def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/uint16_1_4660.tif") as im:
@@ -807,7 +839,7 @@ class TestFileTiff:
not os.path.exists("Tests/images/string_dimension.tiff"),
reason="Extra image files not installed",
)
- def test_string_dimension(self):
+ def test_string_dimension(self) -> None:
# Assert that an error is raised if one of the dimensions is a string
with Image.open("Tests/images/string_dimension.tiff") as im:
with pytest.raises(OSError):
@@ -815,7 +847,7 @@ class TestFileTiff:
@pytest.mark.timeout(6)
@pytest.mark.filterwarnings("ignore:Truncated File Read")
- def test_timeout(self):
+ def test_timeout(self) -> None:
with Image.open("Tests/images/timeout-6646305047838720") as im:
ImageFile.LOAD_TRUNCATED_IMAGES = True
im.load()
@@ -828,7 +860,7 @@ class TestFileTiff:
],
)
@pytest.mark.timeout(2)
- def test_oom(self, test_file):
+ def test_oom(self, test_file) -> None:
with pytest.raises(UnidentifiedImageError):
with pytest.warns(UserWarning):
with Image.open(test_file):
@@ -837,7 +869,7 @@ class TestFileTiff:
@pytest.mark.skipif(not is_win32(), reason="Windows only")
class TestFileTiffW32:
- def test_fd_leak(self, tmp_path):
+ def test_fd_leak(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.tif")
# this is an mmaped file.
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index b7d100e7a..bb6225d07 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
import io
import struct
+from pathlib import Path
import pytest
@@ -11,7 +14,7 @@ from .helper import assert_deep_equal, hopper
TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}
-def test_rt_metadata(tmp_path):
+def test_rt_metadata(tmp_path: Path) -> None:
"""Test writing arbitrary metadata into the tiff image directory
Use case is ImageJ private tags, one numeric, one arbitrary
data. https://github.com/python-pillow/Pillow/issues/291
@@ -77,7 +80,7 @@ def test_rt_metadata(tmp_path):
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
-def test_read_metadata():
+def test_read_metadata() -> None:
with Image.open("Tests/images/hopper_g4.tif") as img:
assert {
"YResolution": IFDRational(4294967295, 113653537),
@@ -118,10 +121,11 @@ def test_read_metadata():
} == img.tag.named()
-def test_write_metadata(tmp_path):
+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")
+ del img.tag[278]
img.save(f, tiffinfo=img.tag)
original = img.tag_v2.named()
@@ -154,13 +158,15 @@ def test_write_metadata(tmp_path):
assert value == reloaded[tag], f"{tag} didn't roundtrip"
-def test_change_stripbytecounts_tag_type(tmp_path):
+def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
out = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper.tif") as im:
info = im.tag_v2
+ del info[278]
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
im = im.resize((500, 500))
+ info[TiffImagePlugin.IMAGEWIDTH] = im.width
# STRIPBYTECOUNTS can be a SHORT or a LONG
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
@@ -171,19 +177,19 @@ def test_change_stripbytecounts_tag_type(tmp_path):
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
-def test_no_duplicate_50741_tag():
+def test_no_duplicate_50741_tag() -> None:
assert TAG_IDS["MakerNoteSafety"] == 50741
assert TAG_IDS["BestQualityScale"] == 50780
-def test_iptc(tmp_path):
+def test_iptc(tmp_path: Path) -> None:
out = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper.Lab.tif") as im:
im.save(out)
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
-def test_writing_other_types_to_ascii(value, expected, tmp_path):
+def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271]
@@ -200,7 +206,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path):
@pytest.mark.parametrize("value", (1, IFDRational(1)))
-def test_writing_other_types_to_bytes(value, tmp_path):
+def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None:
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
@@ -216,7 +222,7 @@ def test_writing_other_types_to_bytes(value, tmp_path):
assert reloaded.tag_v2[700] == b"\x01"
-def test_writing_other_types_to_undefined(tmp_path):
+def test_writing_other_types_to_undefined(tmp_path: Path) -> None:
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
@@ -232,7 +238,7 @@ def test_writing_other_types_to_undefined(tmp_path):
assert reloaded.tag_v2[33723] == b"1"
-def test_undefined_zero(tmp_path):
+def test_undefined_zero(tmp_path: Path) -> None:
# Check that the tag has not been changed since this test was created
tag = TiffTags.TAGS_V2[45059]
assert tag.type == TiffTags.UNDEFINED
@@ -247,7 +253,7 @@ def test_undefined_zero(tmp_path):
assert info[45059] == original
-def test_empty_metadata():
+def test_empty_metadata() -> None:
f = io.BytesIO(b"II*\x00\x08\x00\x00\x00")
head = f.read(8)
info = TiffImagePlugin.ImageFileDirectory(head)
@@ -256,7 +262,7 @@ def test_empty_metadata():
info.load(f)
-def test_iccprofile(tmp_path):
+def test_iccprofile(tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/1462
out = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
@@ -267,7 +273,7 @@ def test_iccprofile(tmp_path):
assert im.info["icc_profile"] == reloaded.info["icc_profile"]
-def test_iccprofile_binary():
+def test_iccprofile_binary() -> None:
# https://github.com/python-pillow/Pillow/issues/1526
# We should be able to load this,
# but probably won't be able to save it.
@@ -277,19 +283,19 @@ def test_iccprofile_binary():
assert im.info["icc_profile"]
-def test_iccprofile_save_png(tmp_path):
+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")
im.save(outfile)
-def test_iccprofile_binary_save_png(tmp_path):
+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")
im.save(outfile)
-def test_exif_div_zero(tmp_path):
+def test_exif_div_zero(tmp_path: Path) -> None:
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
info[41988] = TiffImagePlugin.IFDRational(0, 0)
@@ -302,7 +308,7 @@ def test_exif_div_zero(tmp_path):
assert 0 == reloaded.tag_v2[41988].denominator
-def test_ifd_unsigned_rational(tmp_path):
+def test_ifd_unsigned_rational(tmp_path: Path) -> None:
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
@@ -333,7 +339,7 @@ def test_ifd_unsigned_rational(tmp_path):
assert 1 == reloaded.tag_v2[41493].denominator
-def test_ifd_signed_rational(tmp_path):
+def test_ifd_signed_rational(tmp_path: Path) -> None:
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
@@ -376,7 +382,7 @@ def test_ifd_signed_rational(tmp_path):
assert -1 == reloaded.tag_v2[37380].denominator
-def test_ifd_signed_long(tmp_path):
+def test_ifd_signed_long(tmp_path: Path) -> None:
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
@@ -389,7 +395,7 @@ def test_ifd_signed_long(tmp_path):
assert reloaded.tag_v2[37000] == -60000
-def test_empty_values():
+def test_empty_values() -> None:
data = io.BytesIO(
b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00"
@@ -404,7 +410,7 @@ def test_empty_values():
assert 33432 in info
-def test_photoshop_info(tmp_path):
+def test_photoshop_info(tmp_path: Path) -> None:
with Image.open("Tests/images/issue_2278.tif") as im:
assert len(im.tag_v2[34377]) == 70
assert isinstance(im.tag_v2[34377], bytes)
@@ -415,7 +421,7 @@ def test_photoshop_info(tmp_path):
assert isinstance(reloaded.tag_v2[34377], bytes)
-def test_too_many_entries():
+def test_too_many_entries() -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
# 277: ("SamplesPerPixel", SHORT, 1),
@@ -427,7 +433,7 @@ def test_too_many_entries():
assert ifd[277] == 4
-def test_tag_group_data():
+def test_tag_group_data() -> None:
base_ifd = TiffImagePlugin.ImageFileDirectory_v2()
interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965)
for ifd in (base_ifd, interop_ifd):
@@ -441,7 +447,7 @@ def test_tag_group_data():
assert base_ifd.tagtype[2] != interop_ifd.tagtype[256]
-def test_empty_subifd(tmp_path):
+def test_empty_subifd(tmp_path: Path) -> None:
out = str(tmp_path / "temp.jpg")
im = hopper()
diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py
index 4be46e9d6..b34975e83 100644
--- a/Tests/test_file_wal.py
+++ b/Tests/test_file_wal.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from PIL import WalImageFile
from .helper import assert_image_equal_tofile
@@ -5,7 +7,7 @@ from .helper import assert_image_equal_tofile
TEST_FILE = "Tests/images/hopper.wal"
-def test_open():
+def test_open() -> None:
with WalImageFile.open(TEST_FILE) as im:
assert im.format == "WAL"
assert im.format_description == "Quake2 Texture"
@@ -17,7 +19,7 @@ def test_open():
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
-def test_load():
+def test_load() -> None:
with WalImageFile.open(TEST_FILE) as im:
assert im.load()[0, 0] == 122
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
index 30938e971..249846da4 100644
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -1,7 +1,10 @@
+from __future__ import annotations
+
import io
import re
import sys
import warnings
+from pathlib import Path
import pytest
@@ -24,7 +27,7 @@ except ImportError:
class TestUnsupportedWebp:
- def test_unsupported(self):
+ def test_unsupported(self) -> None:
if HAVE_WEBP:
WebPImagePlugin.SUPPORTED = False
@@ -40,15 +43,15 @@ class TestUnsupportedWebp:
@skip_unless_feature("webp")
class TestFileWebp:
- def setup_method(self):
+ def setup_method(self) -> None:
self.rgb_mode = "RGB"
- def test_version(self):
+ def test_version(self) -> None:
_webp.WebPDecoderVersion()
_webp.WebPDecoderBuggyAlpha()
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp"))
- def test_read_rgb(self):
+ def test_read_rgb(self) -> None:
"""
Can we read a RGB mode WebP file without error?
Does it have the bits we expect?
@@ -65,7 +68,7 @@ class TestFileWebp:
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
- def _roundtrip(self, tmp_path, mode, epsilon, args={}):
+ def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None:
temp_file = str(tmp_path / "temp.webp")
hopper(mode).save(temp_file, **args)
@@ -91,7 +94,7 @@ class TestFileWebp:
target = target.convert(self.rgb_mode)
assert_image_similar(image, target, epsilon)
- def test_write_rgb(self, tmp_path):
+ def test_write_rgb(self, tmp_path: Path) -> None:
"""
Can we write a RGB mode file to webp without error?
Does it have the bits we expect?
@@ -99,7 +102,7 @@ class TestFileWebp:
self._roundtrip(tmp_path, self.rgb_mode, 12.5)
- def test_write_method(self, tmp_path):
+ def test_write_method(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6})
buffer_no_args = io.BytesIO()
@@ -110,7 +113,7 @@ class TestFileWebp:
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
@skip_unless_feature("webp_anim")
- def test_save_all(self, tmp_path):
+ def test_save_all(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
@@ -122,14 +125,14 @@ class TestFileWebp:
reloaded.seek(1)
assert_image_similar(im2, reloaded, 1)
- def test_icc_profile(self, tmp_path):
+ def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
if _webp.HAVE_WEBPANIM:
self._roundtrip(
tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
)
- def test_write_unsupported_mode_L(self, tmp_path):
+ def test_write_unsupported_mode_L(self, tmp_path: Path) -> None:
"""
Saving a black-and-white file to WebP format should work, and be
similar to the original file.
@@ -137,7 +140,7 @@ class TestFileWebp:
self._roundtrip(tmp_path, "L", 10.0)
- def test_write_unsupported_mode_P(self, tmp_path):
+ def test_write_unsupported_mode_P(self, tmp_path: Path) -> None:
"""
Saving a palette-based file to WebP format should work, and be
similar to the original file.
@@ -146,14 +149,14 @@ class TestFileWebp:
self._roundtrip(tmp_path, "P", 50.0)
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
- def test_write_encoding_error_message(self, tmp_path):
+ def test_write_encoding_error_message(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (15000, 15000))
with pytest.raises(ValueError) as e:
im.save(temp_file, method=0)
assert str(e.value) == "encoding error 6"
- def test_WebPEncode_with_invalid_args(self):
+ def test_WebPEncode_with_invalid_args(self) -> None:
"""
Calling encoder functions with no arguments should result in an error.
"""
@@ -164,7 +167,7 @@ class TestFileWebp:
with pytest.raises(TypeError):
_webp.WebPEncode()
- def test_WebPDecode_with_invalid_args(self):
+ def test_WebPDecode_with_invalid_args(self) -> None:
"""
Calling decoder functions with no arguments should result in an error.
"""
@@ -175,14 +178,14 @@ class TestFileWebp:
with pytest.raises(TypeError):
_webp.WebPDecode()
- def test_no_resource_warning(self, tmp_path):
+ def test_no_resource_warning(self, tmp_path: Path) -> None:
file_path = "Tests/images/hopper.webp"
with Image.open(file_path) as image:
temp_file = str(tmp_path / "temp.webp")
with warnings.catch_warnings():
image.save(temp_file)
- def test_file_pointer_could_be_reused(self):
+ def test_file_pointer_could_be_reused(self) -> None:
file_path = "Tests/images/hopper.webp"
with open(file_path, "rb") as blob:
Image.open(blob).load()
@@ -193,14 +196,14 @@ class TestFileWebp:
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
)
@skip_unless_feature("webp_anim")
- def test_invalid_background(self, background, tmp_path):
+ def test_invalid_background(self, background, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = hopper()
with pytest.raises(OSError):
im.save(temp_file, save_all=True, append_images=[im], background=background)
@skip_unless_feature("webp_anim")
- def test_background_from_gif(self, tmp_path):
+ def test_background_from_gif(self, tmp_path: Path) -> None:
# Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im:
out_webp = str(tmp_path / "temp.webp")
@@ -225,7 +228,7 @@ class TestFileWebp:
assert difference < 5
@skip_unless_feature("webp_anim")
- def test_duration(self, tmp_path):
+ def test_duration(self, tmp_path: Path) -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im:
assert im.info["duration"] == 1000
@@ -236,7 +239,7 @@ class TestFileWebp:
reloaded.load()
assert reloaded.info["duration"] == 1000
- def test_roundtrip_rgba_palette(self, tmp_path):
+ def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py
index 5970fd2a3..a95434624 100644
--- a/Tests/test_file_webp_alpha.py
+++ b/Tests/test_file_webp_alpha.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -12,12 +16,12 @@ from .helper import (
_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed")
-def setup_module():
+def setup_module() -> None:
if _webp.WebPDecoderBuggyAlpha():
pytest.skip("Buggy early version of WebP installed, not testing transparency")
-def test_read_rgba():
+def test_read_rgba() -> None:
"""
Can we read an RGBA mode file without error?
Does it have the bits we expect?
@@ -37,7 +41,7 @@ def test_read_rgba():
assert_image_similar_tofile(image, "Tests/images/transparent.png", 20.0)
-def test_write_lossless_rgb(tmp_path):
+def test_write_lossless_rgb(tmp_path: Path) -> None:
"""
Can we write an RGBA mode file with lossless compression without error?
Does it have the bits we expect?
@@ -66,7 +70,7 @@ def test_write_lossless_rgb(tmp_path):
assert_image_equal(image, pil_image)
-def test_write_rgba(tmp_path):
+def test_write_rgba(tmp_path: Path) -> None:
"""
Can we write a RGBA mode file to WebP without error.
Does it have the bits we expect?
@@ -97,7 +101,7 @@ def test_write_rgba(tmp_path):
assert_image_similar(image, pil_image, 1.0)
-def test_keep_rgb_values_when_transparent(tmp_path):
+def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None:
"""
Saving transparent pixels should retain their original RGB values
when using the "exact" parameter.
@@ -126,7 +130,7 @@ def test_keep_rgb_values_when_transparent(tmp_path):
assert_image_equal(reloaded.convert("RGB"), image)
-def test_write_unsupported_mode_PA(tmp_path):
+def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
"""
Saving a palette-based file with transparency to WebP format
should work, and be similar to the original file.
diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py
index 2fd5e5484..9a730f1f9 100644
--- a/Tests/test_file_webp_animated.py
+++ b/Tests/test_file_webp_animated.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from packaging.version import parse as parse_version
@@ -16,7 +20,7 @@ pytestmark = [
]
-def test_n_frames():
+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:
@@ -28,7 +32,7 @@ def test_n_frames():
assert im.is_animated
-def test_write_animation_L(tmp_path):
+def test_write_animation_L(tmp_path: Path) -> None:
"""
Convert an animated GIF to animated WebP, then compare the frame count, and first
and last frames to ensure they're visually similar.
@@ -58,13 +62,13 @@ def test_write_animation_L(tmp_path):
assert_image_similar(im, orig.convert("RGBA"), 32.9)
-def test_write_animation_RGB(tmp_path):
+def test_write_animation_RGB(tmp_path: Path) -> None:
"""
Write an animated WebP from RGB frames, and ensure the frames
are visually similar to the originals.
"""
- def check(temp_file):
+ def check(temp_file) -> None:
with Image.open(temp_file) as im:
assert im.n_frames == 2
@@ -103,7 +107,7 @@ def test_write_animation_RGB(tmp_path):
check(temp_file2)
-def test_timestamp_and_duration(tmp_path):
+def test_timestamp_and_duration(tmp_path: Path) -> None:
"""
Try passing a list of durations, and make sure the encoded
timestamps and durations are correct.
@@ -134,7 +138,7 @@ def test_timestamp_and_duration(tmp_path):
ts += durations[frame]
-def test_float_duration(tmp_path):
+def test_float_duration(tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
with Image.open("Tests/images/iss634.apng") as im:
assert im.info["duration"] == 70.0
@@ -146,7 +150,7 @@ def test_float_duration(tmp_path):
assert reloaded.info["duration"] == 70
-def test_seeking(tmp_path):
+def test_seeking(tmp_path: Path) -> None:
"""
Create an animated WebP file, and then try seeking through frames in reverse-order,
verifying the timestamps and durations are correct.
@@ -177,7 +181,7 @@ def test_seeking(tmp_path):
ts -= dur
-def test_seek_errors():
+def test_seek_errors() -> None:
with Image.open("Tests/images/iss634.webp") as im:
with pytest.raises(EOFError):
im.seek(-1)
diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py
index 2da443628..32e29de56 100644
--- a/Tests/test_file_webp_lossless.py
+++ b/Tests/test_file_webp_lossless.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -8,7 +12,7 @@ _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed")
RGB_MODE = "RGB"
-def test_write_lossless_rgb(tmp_path):
+def test_write_lossless_rgb(tmp_path: Path) -> None:
if _webp.WebPDecoderVersion() < 0x0200:
pytest.skip("lossless not included")
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index dd47be8b2..fea196941 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
import pytest
@@ -17,7 +20,7 @@ except ImportError:
ElementTree = None
-def test_read_exif_metadata():
+def test_read_exif_metadata() -> None:
file_path = "Tests/images/flower.webp"
with Image.open(file_path) as image:
assert image.format == "WEBP"
@@ -35,7 +38,7 @@ def test_read_exif_metadata():
assert exif_data == expected_exif
-def test_read_exif_metadata_without_prefix():
+def test_read_exif_metadata_without_prefix() -> None:
with Image.open("Tests/images/flower2.webp") as im:
# Assert prefix is not present
assert im.info["exif"][:6] != b"Exif\x00\x00"
@@ -47,7 +50,7 @@ def test_read_exif_metadata_without_prefix():
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
-def test_write_exif_metadata():
+def test_write_exif_metadata() -> None:
file_path = "Tests/images/flower.jpg"
test_buffer = BytesIO()
with Image.open(file_path) as image:
@@ -61,7 +64,7 @@ def test_write_exif_metadata():
assert webp_exif == expected_exif[6:], "WebP EXIF didn't match"
-def test_read_icc_profile():
+def test_read_icc_profile() -> None:
file_path = "Tests/images/flower2.webp"
with Image.open(file_path) as image:
assert image.format == "WEBP"
@@ -78,7 +81,7 @@ def test_read_icc_profile():
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
-def test_write_icc_metadata():
+def test_write_icc_metadata() -> None:
file_path = "Tests/images/flower2.jpg"
test_buffer = BytesIO()
with Image.open(file_path) as image:
@@ -98,7 +101,7 @@ def test_write_icc_metadata():
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
-def test_read_no_exif():
+def test_read_no_exif() -> None:
file_path = "Tests/images/flower.jpg"
test_buffer = BytesIO()
with Image.open(file_path) as image:
@@ -111,7 +114,7 @@ def test_read_no_exif():
assert not webp_image._getexif()
-def test_getxmp():
+def test_getxmp() -> None:
with Image.open("Tests/images/flower.webp") as im:
assert "xmp" not in im.info
assert im.getxmp() == {}
@@ -131,7 +134,7 @@ def test_getxmp():
@skip_unless_feature("webp_anim")
-def test_write_animated_metadata(tmp_path):
+def test_write_animated_metadata(tmp_path: Path) -> None:
iccp_data = b""
exif_data = b""
xmp_data = b""
diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py
index 7c8b54fd1..b43e3f296 100644
--- a/Tests/test_file_wmf.py
+++ b/Tests/test_file_wmf.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, WmfImagePlugin
@@ -5,7 +9,7 @@ from PIL import Image, WmfImagePlugin
from .helper import assert_image_similar_tofile, hopper
-def test_load_raw():
+def test_load_raw() -> None:
# Test basic EMF open and rendering
with Image.open("Tests/images/drawing.emf") as im:
if hasattr(Image.core, "drawwmf"):
@@ -23,17 +27,17 @@ def test_load_raw():
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0)
-def test_load():
+def test_load() -> None:
with Image.open("Tests/images/drawing.emf") as im:
if hasattr(Image.core, "drawwmf"):
assert im.load()[0, 0] == (255, 255, 255)
-def test_register_handler(tmp_path):
+def test_register_handler(tmp_path: Path) -> None:
class TestHandler:
methodCalled = False
- def save(self, im, fp, filename):
+ def save(self, im, fp, filename) -> None:
self.methodCalled = True
handler = TestHandler()
@@ -49,12 +53,12 @@ def test_register_handler(tmp_path):
WmfImagePlugin.register_handler(original_handler)
-def test_load_float_dpi():
+def test_load_float_dpi() -> None:
with Image.open("Tests/images/drawing.emf") as im:
assert im.info["dpi"] == 1423.7668161434979
-def test_load_set_dpi():
+def test_load_set_dpi() -> None:
with Image.open("Tests/images/drawing.wmf") as im:
assert im.size == (82, 82)
@@ -66,7 +70,7 @@ def test_load_set_dpi():
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
-def test_save(ext, tmp_path):
+def test_save(ext, tmp_path: Path) -> None:
im = hopper()
tmpfile = str(tmp_path / ("temp" + ext))
diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py
index d2c05b78a..44dd2541f 100644
--- a/Tests/test_file_xbm.py
+++ b/Tests/test_file_xbm.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
import pytest
@@ -30,14 +33,14 @@ static char basic_bits[] = {
"""
-def test_pil151():
+def test_pil151() -> None:
with Image.open(BytesIO(PIL151)) as im:
im.load()
assert im.mode == "1"
assert im.size == (32, 32)
-def test_open():
+def test_open() -> None:
# Arrange
# Created with `convert hopper.png hopper.xbm`
filename = "Tests/images/hopper.xbm"
@@ -49,7 +52,7 @@ def test_open():
assert im.size == (128, 128)
-def test_open_filename_with_underscore():
+def test_open_filename_with_underscore() -> None:
# Arrange
# Created with `convert hopper.png hopper_underscore.xbm`
filename = "Tests/images/hopper_underscore.xbm"
@@ -61,14 +64,14 @@ def test_open_filename_with_underscore():
assert im.size == (128, 128)
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
XbmImagePlugin.XbmImageFile(invalid_file)
-def test_save_wrong_mode(tmp_path):
+def test_save_wrong_mode(tmp_path: Path) -> None:
im = hopper()
out = str(tmp_path / "temp.xbm")
@@ -76,7 +79,7 @@ def test_save_wrong_mode(tmp_path):
im.save(out)
-def test_hotspot(tmp_path):
+def test_hotspot(tmp_path: Path) -> None:
im = hopper("1")
out = str(tmp_path / "temp.xbm")
diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py
index 8595b07eb..26afe93f4 100644
--- a/Tests/test_file_xpm.py
+++ b/Tests/test_file_xpm.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, XpmImagePlugin
@@ -7,7 +9,7 @@ from .helper import assert_image_similar, hopper
TEST_FILE = "Tests/images/hopper.xpm"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(TEST_FILE) as im:
im.load()
assert im.mode == "P"
@@ -18,14 +20,14 @@ def test_sanity():
assert_image_similar(im.convert("RGB"), hopper("RGB"), 60)
-def test_invalid_file():
+def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
XpmImagePlugin.XpmImageFile(invalid_file)
-def test_load_read():
+def test_load_read() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
dummy_bytes = 1
diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py
index 9efe7ec14..6b8115930 100644
--- a/Tests/test_file_xvthumb.py
+++ b/Tests/test_file_xvthumb.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, XVThumbImagePlugin
@@ -7,7 +9,7 @@ from .helper import assert_image_similar, hopper
TEST_FILE = "Tests/images/hopper.p7"
-def test_open():
+def test_open() -> None:
# Act
with Image.open(TEST_FILE) as im:
# Assert
@@ -18,7 +20,7 @@ def test_open():
assert_image_similar(im, im_hopper, 9)
-def test_unexpected_eof():
+def test_unexpected_eof() -> None:
# Test unexpected EOF reading XV thumbnail file
# Arrange
bad_file = "Tests/images/hopper_bad.p7"
@@ -28,7 +30,7 @@ def test_unexpected_eof():
XVThumbImagePlugin.XVThumbImageFile(bad_file)
-def test_invalid_file():
+def test_invalid_file() -> None:
# Arrange
invalid_file = "Tests/images/flower.jpg"
diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py
index 1e7caee32..136070f9e 100644
--- a/Tests/test_font_bdf.py
+++ b/Tests/test_font_bdf.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import BdfFontFile, FontFile
@@ -5,7 +7,7 @@ from PIL import BdfFontFile, FontFile
filename = "Tests/images/courB08.bdf"
-def test_sanity():
+def test_sanity() -> None:
with open(filename, "rb") as test_file:
font = BdfFontFile.BdfFontFile(test_file)
@@ -13,7 +15,7 @@ def test_sanity():
assert len([_f for _f in font.glyph if _f]) == 190
-def test_invalid_file():
+def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):
BdfFontFile.BdfFontFile(fp)
diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py
index 27663f396..b82340ef7 100644
--- a/Tests/test_font_crash.py
+++ b/Tests/test_font_crash.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageDraw, ImageFont
@@ -6,7 +8,7 @@ from .helper import skip_unless_feature
class TestFontCrash:
- def _fuzz_font(self, font):
+ def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
# from fuzzers.fuzz_font
font.getbbox("ABC")
font.getmask("test text")
@@ -16,7 +18,7 @@ class TestFontCrash:
draw.text((10, 10), "Test Text", font=font, fill="#000")
@skip_unless_feature("freetype2")
- def test_segfault(self):
+ def test_segfault(self) -> None:
with pytest.raises(OSError):
font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
self._fuzz_font(font)
diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py
index 38f7ddac5..241f455b8 100644
--- a/Tests/test_font_leaks.py
+++ b/Tests/test_font_leaks.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from PIL import Image, ImageDraw, ImageFont
from .helper import PillowLeakTestCase, skip_unless_feature
@@ -8,7 +10,7 @@ class TestTTypeFontLeak(PillowLeakTestCase):
iterations = 10
mem_limit = 4096 # k
- def _test_font(self, font):
+ def _test_font(self, font: ImageFont.FreeTypeFont) -> None:
im = Image.new("RGB", (255, 255), "white")
draw = ImageDraw.ImageDraw(im)
self._test_leak(
@@ -18,7 +20,7 @@ class TestTTypeFontLeak(PillowLeakTestCase):
)
@skip_unless_feature("freetype2")
- def test_leak(self):
+ def test_leak(self) -> None:
ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
self._test_font(ttype)
@@ -28,6 +30,6 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
iterations = 100
mem_limit = 1024 # k
- def test_leak(self):
+ def test_leak(self) -> None:
default_font = ImageFont.load_default()
self._test_font(default_font)
diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py
index 815ef1d92..997809e46 100644
--- a/Tests/test_font_pcf.py
+++ b/Tests/test_font_pcf.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
import os
+from pathlib import Path
import pytest
@@ -18,7 +21,7 @@ message = "hello, world"
pytestmark = skip_unless_feature("zlib")
-def save_font(request, tmp_path):
+def save_font(request: pytest.FixtureRequest, tmp_path: Path) -> str:
with open(fontname, "rb") as test_file:
font = PcfFontFile.PcfFontFile(test_file)
assert isinstance(font, FontFile.FontFile)
@@ -27,7 +30,7 @@ def save_font(request, tmp_path):
tempname = str(tmp_path / "temp.pil")
- def delete_tempfile():
+ def delete_tempfile() -> None:
try:
os.remove(tempname[:-4] + ".pbm")
except OSError:
@@ -45,11 +48,11 @@ def save_font(request, tmp_path):
return tempname
-def test_sanity(request, tmp_path):
+def test_sanity(request: pytest.FixtureRequest, tmp_path: Path) -> None:
save_font(request, tmp_path)
-def test_less_than_256_characters():
+def test_less_than_256_characters() -> None:
with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file:
font = PcfFontFile.PcfFontFile(test_file)
assert isinstance(font, FontFile.FontFile)
@@ -57,13 +60,13 @@ def test_less_than_256_characters():
assert len([_f for _f in font.glyph if _f]) == 127
-def test_invalid_file():
+def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):
PcfFontFile.PcfFontFile(fp)
-def test_draw(request, tmp_path):
+def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None:
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)
im = Image.new("L", (130, 30), "white")
@@ -72,7 +75,7 @@ def test_draw(request, tmp_path):
assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0)
-def test_textsize(request, tmp_path):
+def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None:
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)
for i in range(255):
@@ -88,7 +91,9 @@ def test_textsize(request, tmp_path):
assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20)
-def _test_high_characters(request, tmp_path, message):
+def _test_high_characters(
+ request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes
+) -> None:
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)
im = Image.new("L", (750, 30), "white")
@@ -97,7 +102,7 @@ def _test_high_characters(request, tmp_path, message):
assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0)
-def test_high_characters(request, tmp_path):
+def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None:
message = "".join(chr(i + 1) for i in range(140, 232))
_test_high_characters(request, tmp_path, message)
# accept bytes instances.
diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py
index 664663fd6..895458d9d 100644
--- a/Tests/test_font_pcf_charsets.py
+++ b/Tests/test_font_pcf_charsets.py
@@ -1,4 +1,8 @@
+from __future__ import annotations
+
import os
+from pathlib import Path
+from typing import TypedDict
import pytest
@@ -12,7 +16,14 @@ from .helper import (
fontname = "Tests/fonts/ter-x20b.pcf"
-charsets = {
+
+class Charset(TypedDict):
+ glyph_count: int
+ message: str
+ image1: str
+
+
+charsets: dict[str, Charset] = {
"iso8859-1": {
"glyph_count": 223,
"message": "hello, world",
@@ -34,7 +45,7 @@ charsets = {
pytestmark = skip_unless_feature("zlib")
-def save_font(request, tmp_path, encoding):
+def save_font(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> str:
with open(fontname, "rb") as test_file:
font = PcfFontFile.PcfFontFile(test_file, encoding)
assert isinstance(font, FontFile.FontFile)
@@ -43,7 +54,7 @@ def save_font(request, tmp_path, encoding):
tempname = str(tmp_path / "temp.pil")
- def delete_tempfile():
+ def delete_tempfile() -> None:
try:
os.remove(tempname[:-4] + ".pbm")
except OSError:
@@ -62,12 +73,12 @@ def save_font(request, tmp_path, encoding):
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
-def test_sanity(request, tmp_path, encoding):
+def test_sanity(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None:
save_font(request, tmp_path, encoding)
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
-def test_draw(request, tmp_path, encoding):
+def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None:
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
im = Image.new("L", (150, 30), "white")
@@ -78,7 +89,9 @@ def test_draw(request, tmp_path, encoding):
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
-def test_textsize(request, tmp_path, encoding):
+def test_textsize(
+ request: pytest.FixtureRequest, tmp_path: Path, encoding: str
+) -> None:
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
for i in range(255):
diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py
new file mode 100644
index 000000000..206499a04
--- /dev/null
+++ b/Tests/test_fontfile.py
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from PIL import FontFile
+
+
+def test_save(tmp_path: Path) -> None:
+ tempname = str(tmp_path / "temp.pil")
+
+ font = FontFile.FontFile()
+ with pytest.raises(ValueError):
+ font.save(tempname)
diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py
index b485e854f..73aaae6e7 100644
--- a/Tests/test_format_hsv.py
+++ b/Tests/test_format_hsv.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import colorsys
import itertools
@@ -19,7 +21,7 @@ def tuple_to_ints(tp):
return int(x * 255.0), int(y * 255.0), int(z * 255.0)
-def test_sanity():
+def test_sanity() -> None:
Image.new("HSV", (100, 100))
@@ -76,7 +78,7 @@ def to_rgb_colorsys(im):
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
-def test_wedge():
+def test_wedge() -> None:
src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR)
im = src.convert("HSV")
comparable = to_hsv_colorsys(src)
@@ -108,7 +110,7 @@ def test_wedge():
)
-def test_convert():
+def test_convert() -> None:
im = hopper("RGB").convert("HSV")
comparable = to_hsv_colorsys(hopper("RGB"))
@@ -126,7 +128,7 @@ def test_convert():
)
-def test_hsv_to_rgb():
+def test_hsv_to_rgb() -> None:
comparable = to_hsv_colorsys(hopper("RGB"))
converted = comparable.convert("RGB")
comparable = to_rgb_colorsys(comparable)
diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py
index 41c8efdf3..4fcc37e88 100644
--- a/Tests/test_format_lab.py
+++ b/Tests/test_format_lab.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from PIL import Image
-def test_white():
+def test_white() -> None:
with Image.open("Tests/images/lab.tif") as i:
i.load()
@@ -22,7 +24,7 @@ def test_white():
assert list(b) == [128] * 100
-def test_green():
+def test_green() -> None:
# l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS
# == RGB: 0, 152, 117
with Image.open("Tests/images/lab-green.tif") as i:
@@ -30,7 +32,7 @@ def test_green():
assert k == (128, 28, 128)
-def test_red():
+def test_red() -> None:
# l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS
# == RGB: 255, 0, 124
with Image.open("Tests/images/lab-red.tif") as i:
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 25c773941..453132adf 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -1,9 +1,13 @@
+from __future__ import annotations
+
import io
+import logging
import os
import shutil
import sys
import tempfile
import warnings
+from pathlib import Path
import pytest
@@ -57,19 +61,19 @@ class TestImage:
"HSV",
),
)
- def test_image_modes_success(self, mode):
+ def test_image_modes_success(self, mode) -> None:
Image.new(mode, (1, 1))
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
- def test_image_modes_fail(self, mode):
+ def test_image_modes_fail(self, mode) -> None:
with pytest.raises(ValueError) as e:
Image.new(mode, (1, 1))
assert str(e.value) == "unrecognized image mode"
- def test_exception_inheritance(self):
+ def test_exception_inheritance(self) -> None:
assert issubclass(UnidentifiedImageError, OSError)
- def test_sanity(self):
+ def test_sanity(self) -> None:
im = Image.new("L", (100, 100))
assert repr(im)[:45] == " None:
with pytest.raises(TypeError):
Image.Image()
- def test_repr_pretty(self):
+ def test_repr_pretty(self) -> None:
class Pretty:
- def text(self, text):
+ def text(self, text) -> None:
self.pretty_output = text
im = Image.new("L", (100, 100))
@@ -109,7 +113,7 @@ class TestImage:
im._repr_pretty_(p, None)
assert p.pretty_output == ""
- def test_open_formats(self):
+ def test_open_formats(self) -> None:
PNGFILE = "Tests/images/hopper.png"
JPGFILE = "Tests/images/hopper.jpg"
@@ -131,7 +135,7 @@ class TestImage:
assert im.mode == "RGB"
assert im.size == (128, 128)
- def test_width_height(self):
+ def test_width_height(self) -> None:
im = Image.new("RGB", (1, 2))
assert im.width == 1
assert im.height == 2
@@ -139,29 +143,29 @@ class TestImage:
with pytest.raises(AttributeError):
im.size = (3, 4)
- def test_set_mode(self):
+ def test_set_mode(self) -> None:
im = Image.new("RGB", (1, 1))
with pytest.raises(AttributeError):
im.mode = "P"
- def test_invalid_image(self):
+ def test_invalid_image(self) -> None:
im = io.BytesIO(b"")
with pytest.raises(UnidentifiedImageError):
with Image.open(im):
pass
- def test_bad_mode(self):
+ def test_bad_mode(self) -> None:
with pytest.raises(ValueError):
with Image.open("filename", "bad mode"):
pass
- def test_stringio(self):
+ def test_stringio(self) -> None:
with pytest.raises(ValueError):
with Image.open(io.StringIO()):
pass
- def test_pathlib(self, tmp_path):
+ def test_pathlib(self, tmp_path: Path) -> None:
from PIL.Image import Path
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
@@ -180,11 +184,11 @@ class TestImage:
os.remove(temp_file)
im.save(Path(temp_file))
- def test_fp_name(self, tmp_path):
+ def test_fp_name(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.jpg")
class FP:
- def write(self, b):
+ def write(self, b) -> None:
pass
fp = FP()
@@ -193,7 +197,7 @@ class TestImage:
im = hopper()
im.save(fp)
- def test_tempfile(self):
+ def test_tempfile(self) -> None:
# see #1460, pathlib support breaks tempfile.TemporaryFile on py27
# Will error out on save on 3.0.0
im = hopper()
@@ -202,13 +206,13 @@ class TestImage:
fp.seek(0)
assert_image_similar_tofile(im, fp, 20)
- def test_unknown_extension(self, tmp_path):
+ 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)
- def test_internals(self):
+ def test_internals(self) -> None:
im = Image.new("L", (100, 100))
im.readonly = 1
im._copy()
@@ -223,7 +227,7 @@ class TestImage:
sys.platform == "cygwin",
reason="Test requires opening an mmaped file for writing",
)
- def test_readonly_save(self, tmp_path):
+ def test_readonly_save(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.bmp")
shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
@@ -231,7 +235,7 @@ class TestImage:
assert im.readonly
im.save(temp_file)
- def test_dump(self, tmp_path):
+ def test_dump(self, tmp_path: Path) -> None:
im = Image.new("L", (10, 10))
im._dump(str(tmp_path / "temp_L.ppm"))
@@ -242,7 +246,7 @@ class TestImage:
with pytest.raises(ValueError):
im._dump(str(tmp_path / "temp_HSV.ppm"))
- def test_comparison_with_other_type(self):
+ def test_comparison_with_other_type(self) -> None:
# Arrange
item = Image.new("RGB", (25, 25), "#000")
num = 12
@@ -252,7 +256,7 @@ class TestImage:
assert item is not None
assert item != num
- def test_expand_x(self):
+ def test_expand_x(self) -> None:
# Arrange
im = hopper()
orig_size = im.size
@@ -265,7 +269,7 @@ class TestImage:
assert im.size[0] == orig_size[0] + 2 * xmargin
assert im.size[1] == orig_size[1] + 2 * xmargin
- def test_expand_xy(self):
+ def test_expand_xy(self) -> None:
# Arrange
im = hopper()
orig_size = im.size
@@ -279,12 +283,12 @@ class TestImage:
assert im.size[0] == orig_size[0] + 2 * xmargin
assert im.size[1] == orig_size[1] + 2 * ymargin
- def test_getbands(self):
+ def test_getbands(self) -> None:
# Assert
assert hopper("RGB").getbands() == ("R", "G", "B")
assert hopper("YCbCr").getbands() == ("Y", "Cb", "Cr")
- def test_getchannel_wrong_params(self):
+ def test_getchannel_wrong_params(self) -> None:
im = hopper()
with pytest.raises(ValueError):
@@ -296,7 +300,7 @@ class TestImage:
with pytest.raises(ValueError):
im.getchannel("1")
- def test_getchannel(self):
+ def test_getchannel(self) -> None:
im = hopper("YCbCr")
Y, Cb, Cr = im.split()
@@ -307,7 +311,7 @@ class TestImage:
assert_image_equal(Cr, im.getchannel(2))
assert_image_equal(Cr, im.getchannel("Cr"))
- def test_getbbox(self):
+ def test_getbbox(self) -> None:
# Arrange
im = hopper()
@@ -317,7 +321,7 @@ class TestImage:
# Assert
assert bbox == (0, 0, 128, 128)
- def test_ne(self):
+ def test_ne(self) -> None:
# Arrange
im1 = Image.new("RGB", (25, 25), "black")
im2 = Image.new("RGB", (25, 25), "white")
@@ -325,7 +329,7 @@ class TestImage:
# Act / Assert
assert im1 != im2
- def test_alpha_composite(self):
+ def test_alpha_composite(self) -> None:
# https://stackoverflow.com/questions/3374878
# Arrange
expected_colors = sorted(
@@ -356,7 +360,7 @@ class TestImage:
img_colors = sorted(img.getcolors())
assert img_colors == expected_colors
- def test_alpha_inplace(self):
+ def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue")
over = Image.new("RGBA", (128, 128), "red")
@@ -408,7 +412,7 @@ class TestImage:
with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), (0, -1))
- def test_register_open_duplicates(self):
+ def test_register_open_duplicates(self) -> None:
# Arrange
factory, accept = Image.OPEN["JPEG"]
id_length = len(Image.ID)
@@ -419,7 +423,7 @@ class TestImage:
# Assert
assert len(Image.ID) == id_length
- def test_registered_extensions_uninitialized(self):
+ def test_registered_extensions_uninitialized(self) -> None:
# Arrange
Image._initialized = 0
@@ -429,7 +433,7 @@ class TestImage:
# Assert
assert Image._initialized == 2
- def test_registered_extensions(self):
+ def test_registered_extensions(self) -> None:
# Arrange
# Open an image to trigger plugin registration
with Image.open("Tests/images/rgb.jpg"):
@@ -443,7 +447,7 @@ class TestImage:
for ext in [".cur", ".icns", ".tif", ".tiff"]:
assert ext in extensions
- def test_effect_mandelbrot(self):
+ def test_effect_mandelbrot(self) -> None:
# Arrange
size = (512, 512)
extent = (-3, -2.5, 2, 2.5)
@@ -456,7 +460,7 @@ class TestImage:
assert im.size == (512, 512)
assert_image_equal_tofile(im, "Tests/images/effect_mandelbrot.png")
- def test_effect_mandelbrot_bad_arguments(self):
+ def test_effect_mandelbrot_bad_arguments(self) -> None:
# Arrange
size = (512, 512)
# Get coordinates the wrong way round:
@@ -468,7 +472,7 @@ class TestImage:
with pytest.raises(ValueError):
Image.effect_mandelbrot(size, extent, quality)
- def test_effect_noise(self):
+ def test_effect_noise(self) -> None:
# Arrange
size = (100, 100)
sigma = 128
@@ -486,7 +490,7 @@ class TestImage:
p4 = im.getpixel((0, 4))
assert_not_all_same([p0, p1, p2, p3, p4])
- def test_effect_spread(self):
+ def test_effect_spread(self) -> None:
# Arrange
im = hopper()
distance = 10
@@ -498,7 +502,7 @@ class TestImage:
assert im.size == (128, 128)
assert_image_similar_tofile(im2, "Tests/images/effect_spread.png", 110)
- def test_effect_spread_zero(self):
+ def test_effect_spread_zero(self) -> None:
# Arrange
im = hopper()
distance = 0
@@ -509,7 +513,7 @@ class TestImage:
# Assert
assert_image_equal(im, im2)
- def test_check_size(self):
+ def test_check_size(self) -> None:
# Checking that the _check_size function throws value errors when we want it to
with pytest.raises(ValueError):
Image.new("RGB", 0) # not a tuple
@@ -538,10 +542,10 @@ class TestImage:
"PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower"
)
@pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
- def test_empty_image(self, size):
+ def test_empty_image(self, size) -> None:
Image.new("RGB", size)
- def test_storage_neg(self):
+ def test_storage_neg(self) -> None:
# Storage.c accepted negative values for xsize, ysize. Was
# test_neg_ppm, but the core function for that has been
# removed Calling directly into core to test the error in
@@ -550,13 +554,13 @@ class TestImage:
with pytest.raises(ValueError):
Image.core.fill("RGB", (2, -2), (0, 0, 0))
- def test_one_item_tuple(self):
+ def test_one_item_tuple(self) -> None:
for mode in ("I", "F", "L"):
im = Image.new(mode, (100, 100), (5,))
px = im.load()
assert px[0, 0] == 5
- def test_linear_gradient_wrong_mode(self):
+ def test_linear_gradient_wrong_mode(self) -> None:
# Arrange
wrong_mode = "RGB"
@@ -565,7 +569,7 @@ class TestImage:
Image.linear_gradient(wrong_mode)
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
- def test_linear_gradient(self, mode):
+ def test_linear_gradient(self, mode) -> None:
# Arrange
target_file = "Tests/images/linear_gradient.png"
@@ -581,7 +585,7 @@ class TestImage:
target = target.convert(mode)
assert_image_equal(im, target)
- def test_radial_gradient_wrong_mode(self):
+ def test_radial_gradient_wrong_mode(self) -> None:
# Arrange
wrong_mode = "RGB"
@@ -590,7 +594,7 @@ class TestImage:
Image.radial_gradient(wrong_mode)
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
- def test_radial_gradient(self, mode):
+ def test_radial_gradient(self, mode) -> None:
# Arrange
target_file = "Tests/images/radial_gradient.png"
@@ -606,7 +610,7 @@ class TestImage:
target = target.convert(mode)
assert_image_equal(im, target)
- def test_register_extensions(self):
+ def test_register_extensions(self) -> None:
test_format = "a"
exts = ["b", "c"]
for ext in exts:
@@ -622,7 +626,7 @@ class TestImage:
assert ext_individual == ext_multiple
- def test_remap_palette(self):
+ def test_remap_palette(self) -> None:
# Test identity transform
with Image.open("Tests/images/hopper.gif") as im:
assert_image_equal(im, im.remap_palette(list(range(256))))
@@ -641,7 +645,7 @@ class TestImage:
with pytest.raises(ValueError):
im.remap_palette(None)
- def test_remap_palette_transparency(self):
+ def test_remap_palette_transparency(self) -> None:
im = Image.new("P", (1, 2), (0, 0, 0))
im.putpixel((0, 1), (255, 0, 0))
im.info["transparency"] = 0
@@ -656,7 +660,7 @@ class TestImage:
im_remapped = im.remap_palette([1, 0])
assert "transparency" not in im_remapped.info
- def test__new(self):
+ def test__new(self) -> None:
im = hopper("RGB")
im_p = hopper("P")
@@ -665,7 +669,7 @@ class TestImage:
blank_p.palette = None
blank_pa.palette = None
- def _make_new(base_image, image, palette_result=None):
+ def _make_new(base_image, image, palette_result=None) -> None:
new_image = base_image._new(image.im)
assert new_image.mode == image.mode
assert new_image.size == image.size
@@ -680,7 +684,7 @@ class TestImage:
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())
- def test_p_from_rgb_rgba(self):
+ def test_p_from_rgb_rgba(self) -> None:
for mode, color in [
("RGB", "#DDEEFF"),
("RGB", (221, 238, 255)),
@@ -690,7 +694,7 @@ class TestImage:
expected = Image.new(mode, (100, 100), color)
assert_image_equal(im.convert(mode), expected)
- def test_no_resource_warning_on_save(self, tmp_path):
+ def test_no_resource_warning_on_save(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/835
# Arrange
test_file = "Tests/images/hopper.png"
@@ -701,7 +705,7 @@ class TestImage:
with warnings.catch_warnings():
im.save(temp_file)
- def test_no_new_file_on_error(self, tmp_path):
+ def test_no_new_file_on_error(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.jpg")
im = Image.new("RGB", (0, 0))
@@ -710,10 +714,10 @@ class TestImage:
assert not os.path.exists(temp_file)
- def test_load_on_nonexclusive_multiframe(self):
+ def test_load_on_nonexclusive_multiframe(self) -> None:
with open("Tests/images/frozenpond.mpo", "rb") as fp:
- def act(fp):
+ def act(fp) -> None:
im = Image.open(fp)
im.load()
@@ -724,7 +728,7 @@ class TestImage:
assert not fp.closed
- def test_empty_exif(self):
+ def test_empty_exif(self) -> None:
with Image.open("Tests/images/exif.png") as im:
exif = im.getexif()
assert dict(exif)
@@ -740,7 +744,7 @@ class TestImage:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_exif_jpeg(self, tmp_path):
+ def test_exif_jpeg(self, tmp_path: Path) -> None:
with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian
exif = im.getexif()
assert 258 not in exif
@@ -786,7 +790,7 @@ class TestImage:
@skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
- def test_exif_webp(self, tmp_path):
+ def test_exif_webp(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.webp") as im:
exif = im.getexif()
assert exif == {}
@@ -796,7 +800,7 @@ class TestImage:
exif[40963] = 455
exif[305] = "Pillow test"
- def check_exif():
+ def check_exif() -> None:
with Image.open(out) as reloaded:
reloaded_exif = reloaded.getexif()
assert reloaded_exif[258] == 8
@@ -808,7 +812,7 @@ class TestImage:
im.save(out, exif=exif, save_all=True)
check_exif()
- def test_exif_png(self, tmp_path):
+ def test_exif_png(self, tmp_path: Path) -> None:
with Image.open("Tests/images/exif.png") as im:
exif = im.getexif()
assert exif == {274: 1}
@@ -824,7 +828,7 @@ class TestImage:
reloaded_exif = reloaded.getexif()
assert reloaded_exif == {258: 8, 40963: 455, 305: "Pillow test"}
- def test_exif_interop(self):
+ def test_exif_interop(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
assert exif.get_ifd(0xA005) == {
@@ -838,7 +842,7 @@ class TestImage:
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005)
- def test_exif_ifd1(self):
+ def test_exif_ifd1(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
assert exif.get_ifd(ExifTags.IFD.IFD1) == {
@@ -850,7 +854,7 @@ class TestImage:
283: 180.0,
}
- def test_exif_ifd(self):
+ def test_exif_ifd(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
del exif.get_ifd(0x8769)[0xA005]
@@ -859,7 +863,7 @@ class TestImage:
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
- def test_exif_load_from_fp(self):
+ def test_exif_load_from_fp(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
data = im.info["exif"]
if data.startswith(b"Exif\x00\x00"):
@@ -880,7 +884,7 @@ class TestImage:
34665: 196,
}
- def test_exif_hide_offsets(self):
+ def test_exif_hide_offsets(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
@@ -906,11 +910,18 @@ class TestImage:
assert exif.get_ifd(0xA005)
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
- def test_zero_tobytes(self, size):
+ def test_zero_tobytes(self, size) -> None:
im = Image.new("RGB", size)
assert im.tobytes() == b""
- def test_has_transparency_data(self):
+ @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
+ def test_zero_frombytes(self, size) -> None:
+ Image.frombytes("RGB", size, b"")
+
+ im = Image.new("RGB", size)
+ im.frombytes(b"")
+
+ def test_has_transparency_data(self) -> None:
for mode in ("1", "L", "P", "RGB"):
im = Image.new(mode, (1, 1))
assert not im.has_transparency_data
@@ -935,7 +946,7 @@ class TestImage:
assert im.palette.mode == "RGBA"
assert im.has_transparency_data
- def test_apply_transparency(self):
+ def test_apply_transparency(self) -> None:
im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 1, 1, 1))
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
@@ -964,7 +975,7 @@ class TestImage:
im.apply_transparency()
assert im.palette.colors[(27, 35, 6, 214)] == 24
- def test_constants(self):
+ def test_constants(self) -> None:
for enum in (
Image.Transpose,
Image.Transform,
@@ -989,28 +1000,42 @@ class TestImage:
"01r_00.pcx",
],
)
- def test_overrun(self, path):
+ def test_overrun(self, path) -> None:
"""For overrun completeness, test as:
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
"""
with Image.open(os.path.join("Tests/images", path)) as im:
try:
im.load()
- assert False
+ pytest.fail()
except OSError as e:
buffer_overrun = str(e) == "buffer overrun when reading image file"
truncated = "image file is truncated" in str(e)
assert buffer_overrun or truncated
- def test_fli_overrun2(self):
+ def test_fli_overrun2(self) -> None:
with Image.open("Tests/images/fli_overrun2.bin") as im:
try:
im.seek(1)
- assert False
+ pytest.fail()
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
+ def test_exit_fp(self) -> None:
+ with Image.new("L", (1, 1)) as im:
+ pass
+ assert not hasattr(im, "fp")
+
+ def test_close_graceful(self, caplog) -> None:
+ with Image.open("Tests/images/hopper.jpg") as im:
+ copy = im.copy()
+ with caplog.at_level(logging.DEBUG):
+ im.close()
+ copy.close()
+ assert len(caplog.records) == 0
+ assert im.fp is None
+
class MockEncoder:
pass
@@ -1023,7 +1048,7 @@ def mock_encode(*args):
class TestRegistry:
- def test_encode_registry(self):
+ def test_encode_registry(self) -> None:
Image.register_encoder("MOCK", mock_encode)
assert "MOCK" in Image.ENCODERS
@@ -1032,6 +1057,6 @@ class TestRegistry:
assert isinstance(enc, MockEncoder)
assert enc.args == ("RGB", "args", "extra")
- def test_encode_registry_fail(self):
+ def test_encode_registry_fail(self) -> None:
with pytest.raises(OSError):
Image._getencoder("RGB", "DoesNotExist", ("args",), extra=("extra",))
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index 2b4fb7733..00cd4e7a9 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import os
import subprocess
import sys
@@ -33,16 +35,16 @@ class AccessTest:
_need_cffi_access = False
@classmethod
- def setup_class(cls):
+ def setup_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._need_cffi_access
@classmethod
- def teardown_class(cls):
+ def teardown_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._init_cffi_access
class TestImagePutPixel(AccessTest):
- def test_sanity(self):
+ def test_sanity(self) -> None:
im1 = hopper()
im2 = Image.new(im1.mode, im1.size, 0)
@@ -79,7 +81,7 @@ class TestImagePutPixel(AccessTest):
assert_image_equal(im1, im2)
- def test_sanity_negative_index(self):
+ def test_sanity_negative_index(self) -> None:
im1 = hopper()
im2 = Image.new(im1.mode, im1.size, 0)
@@ -117,7 +119,7 @@ class TestImagePutPixel(AccessTest):
assert_image_equal(im1, im2)
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
- def test_numpy(self):
+ def test_numpy(self) -> None:
im = hopper()
pix = im.load()
@@ -136,7 +138,7 @@ class TestImageGetPixel(AccessTest):
return (16, 32, 49)
return tuple(range(1, bands + 1))
- def check(self, mode, expected_color=None):
+ def check(self, mode, expected_color=None) -> None:
if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes")
@@ -220,10 +222,10 @@ class TestImageGetPixel(AccessTest):
"YCbCr",
),
)
- def test_basic(self, mode):
+ def test_basic(self, mode) -> None:
self.check(mode)
- def test_list(self):
+ def test_list(self) -> None:
im = hopper()
assert im.getpixel([0, 0]) == (20, 20, 70)
@@ -231,14 +233,14 @@ class TestImageGetPixel(AccessTest):
@pytest.mark.parametrize(
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
)
- def test_signedness(self, mode, expected_color):
+ def test_signedness(self, mode, expected_color) -> None:
# see https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint*
self.check(mode, expected_color)
@pytest.mark.parametrize("mode", ("P", "PA"))
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
- def test_p_putpixel_rgb_rgba(self, mode, color):
+ def test_p_putpixel_rgb_rgba(self, mode, color) -> None:
im = Image.new(mode, (1, 1))
im.putpixel((0, 0), color)
@@ -262,7 +264,7 @@ class TestCffiGetPixel(TestImageGetPixel):
class TestCffi(AccessTest):
_need_cffi_access = True
- def _test_get_access(self, im):
+ def _test_get_access(self, im) -> None:
"""Do we get the same thing as the old pixel access
Using private interfaces, forcing a capi access and
@@ -280,7 +282,7 @@ class TestCffi(AccessTest):
with pytest.raises(ValueError):
access[(access.xsize + 1, access.ysize + 1)]
- def test_get_vs_c(self):
+ def test_get_vs_c(self) -> None:
with pytest.warns(DeprecationWarning):
rgb = hopper("RGB")
rgb.load()
@@ -299,7 +301,7 @@ class TestCffi(AccessTest):
# im = Image.new('I;32B', (10, 10), 2**10)
# self._test_get_access(im)
- def _test_set_access(self, im, color):
+ def _test_set_access(self, im, color) -> None:
"""Are we writing the correct bits into the image?
Using private interfaces, forcing a capi access and
@@ -320,7 +322,7 @@ class TestCffi(AccessTest):
with pytest.raises(ValueError):
access[(0, 0)] = color
- def test_set_vs_c(self):
+ def test_set_vs_c(self) -> None:
rgb = hopper("RGB")
with pytest.warns(DeprecationWarning):
rgb.load()
@@ -343,11 +345,11 @@ class TestCffi(AccessTest):
# self._test_set_access(im, 2**13-1)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
- def test_not_implemented(self):
+ def test_not_implemented(self) -> None:
assert PyAccess.new(hopper("BGR;15")) is None
# ref https://github.com/python-pillow/Pillow/pull/2009
- def test_reference_counting(self):
+ def test_reference_counting(self) -> None:
size = 10
for _ in range(10):
@@ -359,7 +361,7 @@ class TestCffi(AccessTest):
assert px[i, 0] == 0
@pytest.mark.parametrize("mode", ("P", "PA"))
- def test_p_putpixel_rgb_rgba(self, mode):
+ def test_p_putpixel_rgb_rgba(self, mode) -> None:
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1))
with pytest.warns(DeprecationWarning):
@@ -377,7 +379,7 @@ class TestImagePutPixelError(AccessTest):
INVALID_TYPES = ["foo", 1.0, None]
@pytest.mark.parametrize("mode", IMAGE_MODES1)
- def test_putpixel_type_error1(self, mode):
+ def test_putpixel_type_error1(self, mode) -> None:
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"):
@@ -400,14 +402,14 @@ class TestImagePutPixelError(AccessTest):
),
),
)
- def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match):
+ def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None:
im = hopper(mode)
for band_number in band_numbers:
with pytest.raises(TypeError, match=match):
im.putpixel((0, 0), (0,) * band_number)
@pytest.mark.parametrize("mode", IMAGE_MODES2)
- def test_putpixel_type_error2(self, mode):
+ def test_putpixel_type_error2(self, mode) -> None:
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(
@@ -416,7 +418,7 @@ class TestImagePutPixelError(AccessTest):
im.putpixel((0, 0), v)
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
- def test_putpixel_overflow_error(self, mode):
+ def test_putpixel_overflow_error(self, mode) -> None:
im = hopper(mode)
with pytest.raises(OverflowError):
im.putpixel((0, 0), 2**80)
@@ -425,7 +427,7 @@ class TestImagePutPixelError(AccessTest):
class TestEmbeddable:
@pytest.mark.xfail(reason="failing test")
@pytest.mark.skipif(not is_win32(), reason="requires Windows")
- def test_embeddable(self):
+ def test_embeddable(self) -> None:
import ctypes
from setuptools.command.build_ext import new_compiler
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
index ae3518e44..0125ab56a 100644
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from packaging.version import parse as parse_version
@@ -10,12 +12,12 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
im = hopper().resize((128, 100))
-def test_toarray():
+def test_toarray() -> None:
def test(mode):
ai = numpy.array(im.convert(mode))
return ai.shape, ai.dtype.str, ai.nbytes
- def test_with_dtype(dtype):
+ def test_with_dtype(dtype) -> None:
ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype
@@ -44,11 +46,11 @@ def test_toarray():
numpy.array(im_truncated)
-def test_fromarray():
+def test_fromarray() -> None:
class Wrapper:
"""Class with API matching Image.fromarray"""
- def __init__(self, img, arr_params):
+ def __init__(self, img, arr_params) -> None:
self.img = img
self.__array_interface__ = arr_params
@@ -87,7 +89,7 @@ def test_fromarray():
Image.fromarray(wrapped)
-def test_fromarray_palette():
+def test_fromarray_palette() -> None:
# Arrange
i = im.convert("L")
a = numpy.array(i)
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index f5775f09c..f154de123 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -5,8 +9,8 @@ from PIL import Image
from .helper import assert_image, assert_image_equal, assert_image_similar, hopper
-def test_sanity():
- def convert(im, mode):
+def test_sanity() -> None:
+ def convert(im: Image.Image, mode: str) -> None:
out = im.convert(mode)
assert out.mode == mode
assert out.size == im.size
@@ -38,13 +42,13 @@ def test_sanity():
convert(im, output_mode)
-def test_unsupported_conversion():
+def test_unsupported_conversion() -> None:
im = hopper()
with pytest.raises(ValueError):
im.convert("INVALID")
-def test_default():
+def test_default() -> None:
im = hopper("P")
assert im.mode == "P"
converted_im = im.convert()
@@ -60,18 +64,18 @@ def test_default():
# ref https://github.com/python-pillow/Pillow/issues/274
-def _test_float_conversion(im):
+def _test_float_conversion(im: Image.Image) -> None:
orig = im.getpixel((5, 5))
converted = im.convert("F").getpixel((5, 5))
assert orig == converted
-def test_8bit():
+def test_8bit() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
_test_float_conversion(im.convert("L"))
-def test_16bit():
+def test_16bit() -> None:
with Image.open("Tests/images/16bit.cropped.tif") as im:
_test_float_conversion(im)
@@ -81,19 +85,19 @@ def test_16bit():
assert im_i16.getpixel((0, 0)) == 65535
-def test_16bit_workaround():
+def test_16bit_workaround() -> None:
with Image.open("Tests/images/16bit.cropped.tif") as im:
_test_float_conversion(im.convert("I"))
-def test_opaque():
+def test_opaque() -> None:
alpha = hopper("P").convert("PA").getchannel("A")
solid = Image.new("L", (128, 128), 255)
assert_image_equal(alpha, solid)
-def test_rgba_p():
+def test_rgba_p() -> None:
im = hopper("RGBA")
im.putalpha(hopper("L"))
@@ -103,14 +107,14 @@ def test_rgba_p():
assert_image_similar(im, comparable, 20)
-def test_rgba():
+def test_rgba() -> None:
with Image.open("Tests/images/transparent.png") as im:
assert im.mode == "RGBA"
assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5)
-def test_trns_p(tmp_path):
+def test_trns_p(tmp_path: Path) -> None:
im = hopper("P")
im.info["transparency"] = 0
@@ -129,7 +133,7 @@ def test_trns_p(tmp_path):
@pytest.mark.parametrize("mode", ("LA", "PA", "RGBA"))
-def test_trns_p_transparency(mode):
+def test_trns_p_transparency(mode: str) -> None:
# Arrange
im = hopper("P")
im.info["transparency"] = 128
@@ -146,7 +150,7 @@ def test_trns_p_transparency(mode):
assert converted_im.palette is None
-def test_trns_l(tmp_path):
+def test_trns_l(tmp_path: Path) -> None:
im = hopper("L")
im.info["transparency"] = 128
@@ -169,7 +173,7 @@ def test_trns_l(tmp_path):
im_p.save(f)
-def test_trns_RGB(tmp_path):
+def test_trns_RGB(tmp_path: Path) -> None:
im = hopper("RGB")
im.info["transparency"] = im.getpixel((0, 0))
@@ -199,7 +203,7 @@ def test_trns_RGB(tmp_path):
@pytest.mark.parametrize("convert_mode", ("L", "LA", "I"))
-def test_l_macro_rounding(convert_mode):
+def test_l_macro_rounding(convert_mode: str) -> None:
for mode in ("P", "PA"):
im = Image.new(mode, (1, 1))
im.palette.getcolor((0, 1, 2))
@@ -212,7 +216,7 @@ def test_l_macro_rounding(convert_mode):
assert converted_color == 1
-def test_gif_with_rgba_palette_to_p():
+def test_gif_with_rgba_palette_to_p() -> None:
# See https://github.com/python-pillow/Pillow/issues/2433
with Image.open("Tests/images/hopper.gif") as im:
im.info["transparency"] = 255
@@ -224,7 +228,7 @@ def test_gif_with_rgba_palette_to_p():
im_p.load()
-def test_p_la():
+def test_p_la() -> None:
im = hopper("RGBA")
alpha = hopper("L")
im.putalpha(alpha)
@@ -234,7 +238,7 @@ def test_p_la():
assert_image_similar(alpha, comparable, 5)
-def test_p2pa_alpha():
+def test_p2pa_alpha() -> None:
with Image.open("Tests/images/tiny.png") as im:
assert im.mode == "P"
@@ -248,13 +252,13 @@ def test_p2pa_alpha():
assert im_a.getpixel((x, y)) == alpha
-def test_p2pa_palette():
+def test_p2pa_palette() -> None:
with Image.open("Tests/images/tiny.png") as im:
im_pa = im.convert("PA")
assert im_pa.getpalette() == im.getpalette()
-def test_matrix_illegal_conversion():
+def test_matrix_illegal_conversion() -> None:
# Arrange
im = hopper("CMYK")
# fmt: off
@@ -270,7 +274,7 @@ def test_matrix_illegal_conversion():
im.convert(mode="CMYK", matrix=matrix)
-def test_matrix_wrong_mode():
+def test_matrix_wrong_mode() -> None:
# Arrange
im = hopper("L")
# fmt: off
@@ -287,7 +291,7 @@ def test_matrix_wrong_mode():
@pytest.mark.parametrize("mode", ("RGB", "L"))
-def test_matrix_xyz(mode):
+def test_matrix_xyz(mode: str) -> None:
# Arrange
im = hopper("RGB")
im.info["transparency"] = (255, 0, 0)
@@ -315,7 +319,7 @@ def test_matrix_xyz(mode):
assert converted_im.info["transparency"] == 105
-def test_matrix_identity():
+def test_matrix_identity() -> None:
# Arrange
im = hopper("RGB")
# fmt: off
diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py
index cd602fc76..027e5338b 100644
--- a/Tests/test_image_copy.py
+++ b/Tests/test_image_copy.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import copy
import pytest
@@ -8,7 +10,7 @@ from .helper import hopper, skip_unless_feature
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
-def test_copy(mode):
+def test_copy(mode: str) -> None:
cropped_coordinates = (10, 10, 20, 20)
cropped_size = (10, 10)
@@ -37,7 +39,7 @@ def test_copy(mode):
assert out.size == cropped_size
-def test_copy_zero():
+def test_copy_zero() -> None:
im = Image.new("RGB", (0, 0))
out = im.copy()
assert out.mode == im.mode
@@ -45,7 +47,7 @@ def test_copy_zero():
@skip_unless_feature("libtiff")
-def test_deepcopy():
+def test_deepcopy() -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im:
out = copy.deepcopy(im)
assert out.size == (590, 88)
diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py
index daf8c8da1..d095364ba 100644
--- a/Tests/test_image_crop.py
+++ b/Tests/test_image_crop.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -6,7 +8,7 @@ from .helper import assert_image_equal, hopper
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
-def test_crop(mode):
+def test_crop(mode: str) -> None:
im = hopper(mode)
assert_image_equal(im.crop(), im)
@@ -15,8 +17,8 @@ def test_crop(mode):
assert cropped.size == (50, 50)
-def test_wide_crop():
- def crop(*bbox):
+def test_wide_crop() -> None:
+ def crop(*bbox: int) -> tuple[int, ...]:
i = im.crop(bbox)
h = i.histogram()
while h and not h[-1]:
@@ -45,14 +47,14 @@ def test_wide_crop():
@pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2)))
-def test_negative_crop(box):
+def test_negative_crop(box: tuple[int, int, int, int]) -> None:
im = Image.new("RGB", (10, 10))
with pytest.raises(ValueError):
im.crop(box)
-def test_crop_float():
+def test_crop_float() -> None:
# Check cropping floats are rounded to nearest integer
# https://github.com/python-pillow/Pillow/issues/1744
@@ -67,7 +69,7 @@ def test_crop_float():
assert cropped.size == (3, 5)
-def test_crop_crash():
+def test_crop_crash() -> None:
# Image.crop crashes prepatch with an access violation
# apparently a use after free on Windows, see
# https://github.com/python-pillow/Pillow/issues/1077
@@ -85,7 +87,7 @@ def test_crop_crash():
img.load()
-def test_crop_zero():
+def test_crop_zero() -> None:
im = Image.new("RGB", (0, 0), "white")
cropped = im.crop((0, 0, 0, 0))
diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py
index 8b4b44768..54474311a 100644
--- a/Tests/test_image_draft.py
+++ b/Tests/test_image_draft.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from PIL import Image
from .helper import fromstring, skip_unless_feature, tostring
@@ -17,7 +19,7 @@ def draft_roundtrip(in_mode, in_size, req_mode, req_size):
return im
-def test_size():
+def test_size() -> None:
for in_size, req_size, out_size in [
((435, 361), (2048, 2048), (435, 361)), # bigger
((435, 361), (435, 361), (435, 361)), # same
@@ -46,7 +48,7 @@ def test_size():
assert im.size == out_size
-def test_mode():
+def test_mode() -> None:
for in_mode, req_mode, out_mode in [
("RGB", "1", "RGB"),
("RGB", "L", "L"),
@@ -66,7 +68,7 @@ def test_mode():
assert im.mode == out_mode
-def test_several_drafts():
+def test_several_drafts() -> None:
im = draft_roundtrip("L", (128, 128), None, (64, 64))
im.draft(None, (64, 64))
im.load()
diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py
index ea5886e72..01107ae6b 100644
--- a/Tests/test_image_entropy.py
+++ b/Tests/test_image_entropy.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from .helper import hopper
-def test_entropy():
+def test_entropy() -> None:
def entropy(mode):
return hopper(mode).entropy()
diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py
index a7932a351..2b6787933 100644
--- a/Tests/test_image_filter.py
+++ b/Tests/test_image_filter.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageFilter
@@ -34,7 +36,7 @@ from .helper import assert_image_equal, hopper
),
)
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
-def test_sanity(filter_to_apply, mode):
+def test_sanity(filter_to_apply, mode) -> None:
im = hopper(mode)
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
out = im.filter(filter_to_apply)
@@ -43,7 +45,7 @@ def test_sanity(filter_to_apply, mode):
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
-def test_sanity_error(mode):
+def test_sanity_error(mode) -> None:
with pytest.raises(TypeError):
im = hopper(mode)
im.filter("hello")
@@ -51,7 +53,7 @@ def test_sanity_error(mode):
# crashes on small images
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
-def test_crash(size):
+def test_crash(size) -> None:
im = Image.new("RGB", size)
im.filter(ImageFilter.SMOOTH)
@@ -65,7 +67,7 @@ def test_crash(size):
("RGB", ((4, 0, 0), (0, 0, 0))),
),
)
-def test_modefilter(mode, expected):
+def test_modefilter(mode, expected) -> None:
im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9)))
# image is:
@@ -88,7 +90,7 @@ def test_modefilter(mode, expected):
("F", (0.0, 4.0, 8.0)),
),
)
-def test_rankfilter(mode, expected):
+def test_rankfilter(mode, expected) -> None:
im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9)))
# image is:
@@ -104,7 +106,7 @@ def test_rankfilter(mode, expected):
@pytest.mark.parametrize(
"filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)
)
-def test_rankfilter_error(filter):
+def test_rankfilter_error(filter) -> None:
with pytest.raises(ValueError):
im = Image.new("P", (3, 3), None)
im.putdata(list(range(9)))
@@ -115,27 +117,27 @@ def test_rankfilter_error(filter):
im.filter(filter).getpixel((1, 1))
-def test_rankfilter_properties():
+def test_rankfilter_properties() -> None:
rankfilter = ImageFilter.RankFilter(1, 2)
assert rankfilter.size == 1
assert rankfilter.rank == 2
-def test_builtinfilter_p():
+def test_builtinfilter_p() -> None:
builtin_filter = ImageFilter.BuiltinFilter()
with pytest.raises(ValueError):
builtin_filter.filter(hopper("P"))
-def test_kernel_not_enough_coefficients():
+def test_kernel_not_enough_coefficients() -> None:
with pytest.raises(ValueError):
ImageFilter.Kernel((3, 3), (0, 0))
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
-def test_consistency_3x3(mode):
+def test_consistency_3x3(mode) -> None:
with Image.open("Tests/images/hopper.bmp") as source:
reference_name = "hopper_emboss"
reference_name += "_I.png" if mode == "I" else ".bmp"
@@ -161,7 +163,7 @@ def test_consistency_3x3(mode):
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
-def test_consistency_5x5(mode):
+def test_consistency_5x5(mode) -> None:
with Image.open("Tests/images/hopper.bmp") as source:
reference_name = "hopper_emboss_more"
reference_name += "_I.png" if mode == "I" else ".bmp"
@@ -197,7 +199,7 @@ def test_consistency_5x5(mode):
(2, -2),
),
)
-def test_invalid_box_blur_filter(radius):
+def test_invalid_box_blur_filter(radius) -> None:
with pytest.raises(ValueError):
ImageFilter.BoxBlur(radius)
diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py
index c299e4544..6474daba1 100644
--- a/Tests/test_image_frombytes.py
+++ b/Tests/test_image_frombytes.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -6,7 +8,7 @@ from .helper import assert_image_equal, hopper
@pytest.mark.parametrize("data_type", ("bytes", "memoryview"))
-def test_sanity(data_type):
+def test_sanity(data_type) -> None:
im1 = hopper()
data = im1.tobytes()
diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py
index 7fe992353..ea31a9de9 100644
--- a/Tests/test_image_fromqimage.py
+++ b/Tests/test_image_fromqimage.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
import warnings
+from typing import Generator
import pytest
@@ -16,7 +19,7 @@ pytestmark = pytest.mark.skipif(
@pytest.fixture
-def test_images():
+def test_images() -> Generator[Image.Image, None, None]:
ims = [
hopper(),
Image.open("Tests/images/transparent.png"),
@@ -29,7 +32,7 @@ def test_images():
im.close()
-def roundtrip(expected):
+def roundtrip(expected: Image.Image) -> None:
# PIL -> Qt
intermediate = expected.toqimage()
# Qt -> PIL
@@ -41,26 +44,26 @@ def roundtrip(expected):
assert_image_equal(result, expected.convert("RGB"))
-def test_sanity_1(test_images):
+def test_sanity_1(test_images: Generator[Image.Image, None, None]) -> None:
for im in test_images:
roundtrip(im.convert("1"))
-def test_sanity_rgb(test_images):
+def test_sanity_rgb(test_images: Generator[Image.Image, None, None]) -> None:
for im in test_images:
roundtrip(im.convert("RGB"))
-def test_sanity_rgba(test_images):
+def test_sanity_rgba(test_images: Generator[Image.Image, None, None]) -> None:
for im in test_images:
roundtrip(im.convert("RGBA"))
-def test_sanity_l(test_images):
+def test_sanity_l(test_images: Generator[Image.Image, None, None]) -> None:
for im in test_images:
roundtrip(im.convert("L"))
-def test_sanity_p(test_images):
+def test_sanity_p(test_images: Generator[Image.Image, None, None]) -> None:
for im in test_images:
roundtrip(im.convert("P"))
diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py
index 08fc12c1c..887553fc0 100644
--- a/Tests/test_image_getbands.py
+++ b/Tests/test_image_getbands.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from PIL import Image
-def test_getbands():
+def test_getbands() -> None:
assert Image.new("1", (1, 1)).getbands() == ("1",)
assert Image.new("L", (1, 1)).getbands() == ("L",)
assert Image.new("I", (1, 1)).getbands() == ("I",)
diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py
index afca66703..18c6f6579 100644
--- a/Tests/test_image_getbbox.py
+++ b/Tests/test_image_getbbox.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -5,13 +7,13 @@ from PIL import Image
from .helper import hopper
-def test_sanity():
+def test_sanity() -> None:
bbox = hopper().getbbox()
assert isinstance(bbox, tuple)
-def test_bbox():
- def check(im, fill_color):
+def test_bbox() -> None:
+ def check(im: Image.Image, fill_color: int | tuple[int, ...]) -> None:
assert im.getbbox() is None
im.paste(fill_color, (10, 25, 90, 75))
@@ -32,8 +34,8 @@ def test_bbox():
check(im, 255)
for mode in ("RGBA", "RGBa"):
- for color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)):
- im = Image.new(mode, (100, 100), color)
+ for rgba_color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)):
+ im = Image.new(mode, (100, 100), rgba_color)
check(im, (255, 255, 255, 255))
for mode in ("La", "LA", "PA"):
@@ -43,7 +45,7 @@ def test_bbox():
@pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA"))
-def test_bbox_alpha_only_false(mode):
+def test_bbox_alpha_only_false(mode: str) -> None:
im = Image.new(mode, (100, 100))
assert im.getbbox(alpha_only=False) is None
diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py
index 7fd0398f9..8f8870f4f 100644
--- a/Tests/test_image_getcolors.py
+++ b/Tests/test_image_getcolors.py
@@ -1,8 +1,10 @@
+from __future__ import annotations
+
from .helper import hopper
-def test_getcolors():
- def getcolors(mode, limit=None):
+def test_getcolors() -> None:
+ def getcolors(mode: str, limit: int | None = None) -> int | None:
im = hopper(mode)
if limit:
colors = im.getcolors(limit)
@@ -37,7 +39,7 @@ def test_getcolors():
# --------------------------------------------------------------------
-def test_pack():
+def test_pack() -> None:
# Pack problems for small tables (@PIL209)
im = hopper().quantize(3).convert("RGB")
diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py
index 36c81b40f..ac27400be 100644
--- a/Tests/test_image_getdata.py
+++ b/Tests/test_image_getdata.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
from PIL import Image
from .helper import hopper
-def test_sanity():
+def test_sanity() -> None:
data = hopper().getdata()
len(data)
@@ -12,8 +14,8 @@ def test_sanity():
assert data[0] == (20, 20, 70)
-def test_roundtrip():
- def getdata(mode):
+def test_roundtrip() -> None:
+ def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]:
im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
data = im.getdata()
return data[0], len(data), len(list(data))
diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py
index 710794da4..0107fdcc4 100644
--- a/Tests/test_image_getextrema.py
+++ b/Tests/test_image_getextrema.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
from PIL import Image
from .helper import hopper
-def test_extrema():
+def test_extrema() -> None:
def extrema(mode):
return hopper(mode).getextrema()
@@ -18,7 +20,7 @@ def test_extrema():
assert extrema("I;16") == (1, 255)
-def test_true_16():
+def test_true_16() -> None:
with Image.open("Tests/images/16_bit_noise.tif") as im:
assert im.mode == "I;16"
extrema = im.getextrema()
diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py
index 746e63b15..9afa02b0a 100644
--- a/Tests/test_image_getim.py
+++ b/Tests/test_image_getim.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from .helper import hopper
-def test_sanity():
+def test_sanity() -> None:
im = hopper()
type_repr = repr(type(im.getim()))
diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py
index 58a6dacbb..e7304c98f 100644
--- a/Tests/test_image_getpalette.py
+++ b/Tests/test_image_getpalette.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
from PIL import Image
from .helper import hopper
-def test_palette():
+def test_palette() -> None:
def palette(mode):
p = hopper(mode).getpalette()
if p:
@@ -21,7 +23,7 @@ def test_palette():
assert palette("YCbCr") is None
-def test_palette_rawmode():
+def test_palette_rawmode() -> None:
im = Image.new("P", (1, 1))
im.putpalette((1, 2, 3))
diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py
index f65d40708..2b5a758ed 100644
--- a/Tests/test_image_getprojection.py
+++ b/Tests/test_image_getprojection.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
from PIL import Image
from .helper import hopper
-def test_sanity():
+def test_sanity() -> None:
im = hopper()
projection = im.getprojection()
diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py
index 0ee52e724..dbd55d4c2 100644
--- a/Tests/test_image_histogram.py
+++ b/Tests/test_image_histogram.py
@@ -1,8 +1,10 @@
+from __future__ import annotations
+
from .helper import hopper
-def test_histogram():
- def histogram(mode):
+def test_histogram() -> None:
+ def histogram(mode: str) -> tuple[int, int, int]:
h = hopper(mode).histogram()
return len(h), min(h), max(h)
diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py
index f7fe99bb4..5b1a9ee2d 100644
--- a/Tests/test_image_load.py
+++ b/Tests/test_image_load.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import logging
import os
@@ -8,14 +10,14 @@ from PIL import Image
from .helper import hopper
-def test_sanity():
+def test_sanity() -> None:
im = hopper()
pix = im.load()
assert pix[0, 0] == (20, 20, 70)
-def test_close():
+def test_close() -> None:
im = Image.open("Tests/images/hopper.gif")
im.close()
with pytest.raises(ValueError):
@@ -24,7 +26,7 @@ def test_close():
im.getpixel((0, 0))
-def test_close_after_load(caplog):
+def test_close_after_load(caplog) -> None:
im = Image.open("Tests/images/hopper.gif")
im.load()
with caplog.at_level(logging.DEBUG):
@@ -32,7 +34,7 @@ def test_close_after_load(caplog):
assert len(caplog.records) == 0
-def test_contextmanager():
+def test_contextmanager() -> None:
fn = None
with Image.open("Tests/images/hopper.gif") as im:
fn = im.fp.fileno()
@@ -42,7 +44,7 @@ def test_contextmanager():
os.fstat(fn)
-def test_contextmanager_non_exclusive_fp():
+def test_contextmanager_non_exclusive_fp() -> None:
with open("Tests/images/hopper.gif", "rb") as fp:
with Image.open(fp):
pass
diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py
index e4c8bb9df..8e94aafc5 100644
--- a/Tests/test_image_mode.py
+++ b/Tests/test_image_mode.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageMode
@@ -5,7 +7,7 @@ from PIL import Image, ImageMode
from .helper import hopper
-def test_sanity():
+def test_sanity() -> None:
with hopper() as im:
im.mode
@@ -67,7 +69,7 @@ def test_sanity():
)
def test_properties(
mode, expected_base, expected_type, expected_bands, expected_band_names
-):
+) -> None:
assert Image.getmodebase(mode) == expected_base
assert Image.getmodetype(mode) == expected_type
assert Image.getmodebands(mode) == expected_bands
diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py
index 1ab02017d..34a2f8f3d 100644
--- a/Tests/test_image_paste.py
+++ b/Tests/test_image_paste.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -9,7 +11,7 @@ class TestImagingPaste:
masks = {}
size = 128
- def assert_9points_image(self, im, expected):
+ def assert_9points_image(self, im, expected) -> None:
expected = [
point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected
]
@@ -27,7 +29,7 @@ class TestImagingPaste:
]
assert actual == expected
- def assert_9points_paste(self, im, im2, mask, expected):
+ def assert_9points_paste(self, im, im2, mask, expected) -> None:
im3 = im.copy()
im3.paste(im2, (0, 0), mask)
self.assert_9points_image(im3, expected)
@@ -104,7 +106,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_solid(self, mode):
+ def test_image_solid(self, mode) -> None:
im = Image.new(mode, (200, 200), "red")
im2 = getattr(self, "gradient_" + mode)
@@ -114,7 +116,7 @@ class TestImagingPaste:
assert_image_equal(im, im2)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_mask_1(self, mode):
+ def test_image_mask_1(self, mode) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -136,7 +138,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_mask_L(self, mode):
+ def test_image_mask_L(self, mode) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -158,7 +160,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_mask_LA(self, mode):
+ def test_image_mask_LA(self, mode) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -180,7 +182,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_mask_RGBA(self, mode):
+ def test_image_mask_RGBA(self, mode) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -202,7 +204,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_mask_RGBa(self, mode):
+ def test_image_mask_RGBa(self, mode) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -224,7 +226,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_color_solid(self, mode):
+ def test_color_solid(self, mode) -> None:
im = Image.new(mode, (200, 200), "black")
rect = (12, 23, 128 + 12, 128 + 23)
@@ -237,7 +239,7 @@ class TestImagingPaste:
assert sum(head[:255]) == 0
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_color_mask_1(self, mode):
+ def test_color_mask_1(self, mode) -> None:
im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)])
color = (10, 20, 30, 40)[: len(mode)]
@@ -259,7 +261,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_color_mask_L(self, mode):
+ def test_color_mask_L(self, mode) -> None:
im = getattr(self, "gradient_" + mode).copy()
color = "white"
@@ -281,7 +283,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_color_mask_RGBA(self, mode):
+ def test_color_mask_RGBA(self, mode) -> None:
im = getattr(self, "gradient_" + mode).copy()
color = "white"
@@ -303,7 +305,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_color_mask_RGBa(self, mode):
+ def test_color_mask_RGBa(self, mode) -> None:
im = getattr(self, "gradient_" + mode).copy()
color = "white"
@@ -324,7 +326,7 @@ class TestImagingPaste:
],
)
- def test_different_sizes(self):
+ def test_different_sizes(self) -> None:
im = Image.new("RGB", (100, 100))
im2 = Image.new("RGB", (50, 50))
diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py
index c406cb8ec..05f209351 100644
--- a/Tests/test_image_point.py
+++ b/Tests/test_image_point.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
import pytest
from .helper import assert_image_equal, hopper
-def test_sanity():
+def test_sanity() -> None:
im = hopper()
with pytest.raises(ValueError):
@@ -37,7 +39,7 @@ def test_sanity():
im.point(lambda x: x // 2)
-def test_16bit_lut():
+def test_16bit_lut() -> None:
"""Tests for 16 bit -> 8 bit lut for converting I->L images
see https://github.com/python-pillow/Pillow/issues/440
"""
@@ -45,7 +47,7 @@ def test_16bit_lut():
im.point(list(range(256)) * 256, "L")
-def test_f_lut():
+def test_f_lut() -> None:
"""Tests for floating point lut of 8bit gray image"""
im = hopper("L")
lut = [0.5 * float(x) for x in range(256)]
@@ -56,7 +58,7 @@ def test_f_lut():
assert_image_equal(out.convert("L"), im.point(int_lut, "L"))
-def test_f_mode():
+def test_f_mode() -> None:
im = hopper("F")
with pytest.raises(ValueError):
im.point(None)
diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py
index e2dcead34..2c92911d1 100644
--- a/Tests/test_image_putalpha.py
+++ b/Tests/test_image_putalpha.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from PIL import Image
-def test_interface():
+def test_interface() -> None:
im = Image.new("RGBA", (1, 1), (1, 2, 3, 0))
assert im.getpixel((0, 0)) == (1, 2, 3, 0)
@@ -15,7 +17,7 @@ def test_interface():
assert im.getpixel((0, 0)) == (1, 2, 3, 5)
-def test_promote():
+def test_promote() -> None:
im = Image.new("L", (1, 1), 1)
assert im.getpixel((0, 0)) == 1
@@ -38,7 +40,7 @@ def test_promote():
assert im.getpixel((0, 0)) == (1, 2, 3, 4)
-def test_readonly():
+def test_readonly() -> None:
im = Image.new("RGB", (1, 1), (1, 2, 3))
im.readonly = 1
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
index 4e40aec74..103019916 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import sys
from array import array
@@ -8,7 +10,7 @@ from PIL import Image
from .helper import assert_image_equal, hopper
-def test_sanity():
+def test_sanity() -> None:
im1 = hopper()
data = list(im1.getdata())
@@ -27,7 +29,7 @@ def test_sanity():
assert_image_equal(im1, im2)
-def test_long_integers():
+def test_long_integers() -> None:
# see bug-200802-systemerror
def put(value):
im = Image.new("RGBA", (1, 1))
@@ -44,19 +46,19 @@ def test_long_integers():
assert put(sys.maxsize) == (255, 255, 255, 127)
-def test_pypy_performance():
+def test_pypy_performance() -> None:
im = Image.new("L", (256, 256))
im.putdata(list(range(256)) * 256)
-def test_mode_with_L_with_float():
+def test_mode_with_L_with_float() -> None:
im = Image.new("L", (1, 1), 0)
im.putdata([2.0])
assert im.getpixel((0, 0)) == 2
@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
-def test_mode_i(mode):
+def test_mode_i(mode) -> None:
src = hopper("L")
data = list(src.getdata())
im = Image.new(mode, src.size, 0)
@@ -66,7 +68,7 @@ def test_mode_i(mode):
assert list(im.getdata()) == target
-def test_mode_F():
+def test_mode_F() -> None:
src = hopper("L")
data = list(src.getdata())
im = Image.new("F", src.size, 0)
@@ -77,7 +79,7 @@ def test_mode_F():
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
-def test_mode_BGR(mode):
+def test_mode_BGR(mode) -> None:
data = [(16, 32, 49), (32, 32, 98)]
im = Image.new(mode, (1, 2))
im.putdata(data)
@@ -85,7 +87,7 @@ def test_mode_BGR(mode):
assert list(im.getdata()) == data
-def test_array_B():
+def test_array_B() -> None:
# shouldn't segfault
# see https://github.com/python-pillow/Pillow/issues/1008
@@ -96,7 +98,7 @@ def test_array_B():
assert len(im.getdata()) == len(arr)
-def test_array_F():
+def test_array_F() -> None:
# shouldn't segfault
# see https://github.com/python-pillow/Pillow/issues/1008
@@ -107,7 +109,7 @@ def test_array_F():
assert len(im.getdata()) == len(arr)
-def test_not_flattened():
+def test_not_flattened() -> None:
im = Image.new("L", (1, 1))
with pytest.raises(TypeError):
im.putdata([[0]])
diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py
index 376553344..ffe2551d2 100644
--- a/Tests/test_image_putpalette.py
+++ b/Tests/test_image_putpalette.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImagePalette
@@ -5,7 +7,7 @@ from PIL import Image, ImagePalette
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
-def test_putpalette():
+def test_putpalette() -> None:
def palette(mode):
im = hopper(mode).copy()
im.putpalette(list(range(256)) * 3)
@@ -41,7 +43,7 @@ def test_putpalette():
im.putpalette(list(range(256)) * 3)
-def test_imagepalette():
+def test_imagepalette() -> None:
im = hopper("P")
im.putpalette(ImagePalette.negative())
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png")
@@ -55,7 +57,7 @@ def test_imagepalette():
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png")
-def test_putpalette_with_alpha_values():
+def test_putpalette_with_alpha_values() -> None:
with Image.open("Tests/images/transparent.gif") as im:
expected = im.convert("RGBA")
@@ -79,19 +81,19 @@ def test_putpalette_with_alpha_values():
("RGBAX", (1, 2, 3, 4, 0)),
),
)
-def test_rgba_palette(mode, palette):
+def test_rgba_palette(mode, palette) -> None:
im = Image.new("P", (1, 1))
im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3]
assert im.palette.colors == {(1, 2, 3, 4): 0}
-def test_empty_palette():
+def test_empty_palette() -> None:
im = Image.new("P", (1, 1))
assert im.getpalette() == []
-def test_undefined_palette_index():
+def test_undefined_palette_index() -> None:
im = Image.new("P", (1, 1), 3)
im.putpalette((1, 2, 3))
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0)
diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py
index 981753eb9..873a9bb5d 100644
--- a/Tests/test_image_quantize.py
+++ b/Tests/test_image_quantize.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from packaging.version import parse as parse_version
@@ -6,7 +8,7 @@ from PIL import Image, features
from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature
-def test_sanity():
+def test_sanity() -> None:
image = hopper()
converted = image.quantize()
assert converted.mode == "P"
@@ -19,7 +21,7 @@ def test_sanity():
@skip_unless_feature("libimagequant")
-def test_libimagequant_quantize():
+def test_libimagequant_quantize() -> None:
image = hopper()
if is_ppc64le():
libimagequant = parse_version(features.version_feature("libimagequant"))
@@ -31,7 +33,7 @@ def test_libimagequant_quantize():
assert len(converted.getcolors()) == 100
-def test_octree_quantize():
+def test_octree_quantize() -> None:
image = hopper()
converted = image.quantize(100, Image.Quantize.FASTOCTREE)
assert converted.mode == "P"
@@ -39,7 +41,7 @@ def test_octree_quantize():
assert len(converted.getcolors()) == 100
-def test_rgba_quantize():
+def test_rgba_quantize() -> None:
image = hopper("RGBA")
with pytest.raises(ValueError):
image.quantize(method=0)
@@ -47,7 +49,7 @@ def test_rgba_quantize():
assert image.quantize().convert().mode == "RGBA"
-def test_quantize():
+def test_quantize() -> None:
with Image.open("Tests/images/caption_6_33_22.png") as image:
image = image.convert("RGB")
converted = image.quantize()
@@ -55,7 +57,7 @@ def test_quantize():
assert_image_similar(converted.convert("RGB"), image, 1)
-def test_quantize_no_dither():
+def test_quantize_no_dither() -> None:
image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette:
palette = palette.convert("P")
@@ -65,9 +67,9 @@ def test_quantize_no_dither():
assert converted.palette.palette == palette.palette.palette
-def test_quantize_no_dither2():
+def test_quantize_no_dither2() -> None:
im = Image.new("RGB", (9, 1))
- im.putdata(list((p,) * 3 for p in range(0, 36, 4)))
+ im.putdata([(p,) * 3 for p in range(0, 36, 4)])
palette = Image.new("P", (1, 1))
data = (0, 0, 0, 32, 32, 32)
@@ -81,7 +83,7 @@ def test_quantize_no_dither2():
assert px[x, 0] == (0 if x < 5 else 1)
-def test_quantize_dither_diff():
+def test_quantize_dither_diff() -> None:
image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette:
palette = palette.convert("P")
@@ -92,14 +94,14 @@ def test_quantize_dither_diff():
assert dither.tobytes() != nodither.tobytes()
-def test_colors():
+def test_colors() -> None:
im = hopper()
colors = 2
converted = im.quantize(colors)
assert len(converted.palette.palette) == colors * len("RGB")
-def test_transparent_colors_equal():
+def test_transparent_colors_equal() -> None:
im = Image.new("RGBA", (1, 2), (0, 0, 0, 0))
px = im.load()
px[0, 1] = (255, 255, 255, 0)
@@ -118,7 +120,7 @@ def test_transparent_colors_equal():
(Image.Quantize.FASTOCTREE, (0, 0, 0, 0)),
),
)
-def test_palette(method, color):
+def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color)
converted = im.quantize(method=method)
@@ -126,7 +128,7 @@ def test_palette(method, color):
assert converted_px[0, 0] == converted.palette.colors[color]
-def test_small_palette():
+def test_small_palette() -> None:
# Arrange
im = hopper()
diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py
index ae8d740a0..c29830a7e 100644
--- a/Tests/test_image_reduce.py
+++ b/Tests/test_image_reduce.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageMath, ImageMode
@@ -46,7 +48,7 @@ gradients_image.load()
((1, 3), (10, 4)),
),
)
-def test_args_factor(size, expected):
+def test_args_factor(size, expected) -> None:
im = Image.new("L", (10, 10))
assert expected == im.reduce(size).size
@@ -54,7 +56,7 @@ def test_args_factor(size, expected):
@pytest.mark.parametrize(
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
)
-def test_args_factor_error(size, expected_error):
+def test_args_factor_error(size, expected_error) -> None:
im = Image.new("L", (10, 10))
with pytest.raises(expected_error):
im.reduce(size)
@@ -67,7 +69,7 @@ def test_args_factor_error(size, expected_error):
((5, 5, 6, 6), (1, 1)),
),
)
-def test_args_box(size, expected):
+def test_args_box(size, expected) -> None:
im = Image.new("L", (10, 10))
assert expected == im.reduce(2, size).size
@@ -84,14 +86,14 @@ def test_args_box(size, expected):
((5, 0, 5, 10), ValueError),
),
)
-def test_args_box_error(size, expected_error):
+def test_args_box_error(size, expected_error) -> None:
im = Image.new("L", (10, 10))
with pytest.raises(expected_error):
im.reduce(2, size).size
@pytest.mark.parametrize("mode", ("P", "1", "I;16"))
-def test_unsupported_modes(mode):
+def test_unsupported_modes(mode) -> None:
im = Image.new("P", (10, 10))
with pytest.raises(ValueError):
im.reduce(3)
@@ -117,14 +119,16 @@ def get_image(mode):
return im.crop((0, 0, im.width, im.height - 5))
-def compare_reduce_with_box(im, factor):
+def compare_reduce_with_box(im, factor) -> None:
box = (11, 13, 146, 164)
reduced = im.reduce(factor, box=box)
reference = im.crop(box).reduce(factor)
assert reduced == reference
-def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1):
+def compare_reduce_with_reference(
+ im, factor, average_diff: float = 0.4, max_diff: int = 1
+) -> None:
"""Image.reduce() should look very similar to Image.resize(BOX).
A reference image is compiled from a large source area
@@ -169,7 +173,7 @@ def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1):
assert_compare_images(reduced, reference, average_diff, max_diff)
-def assert_compare_images(a, b, max_average_diff, max_diff=255):
+def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None:
assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}"
@@ -197,20 +201,20 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255):
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_L(factor):
+def test_mode_L(factor) -> None:
im = get_image("L")
compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_LA(factor):
+def test_mode_LA(factor) -> None:
im = get_image("LA")
compare_reduce_with_reference(im, factor, 0.8, 5)
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_LA_opaque(factor):
+def test_mode_LA_opaque(factor) -> None:
im = get_image("LA")
# With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255))
@@ -219,27 +223,27 @@ def test_mode_LA_opaque(factor):
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_La(factor):
+def test_mode_La(factor) -> None:
im = get_image("La")
compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_RGB(factor):
+def test_mode_RGB(factor) -> None:
im = get_image("RGB")
compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_RGBA(factor):
+def test_mode_RGBA(factor) -> None:
im = get_image("RGBA")
compare_reduce_with_reference(im, factor, 0.8, 5)
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_RGBA_opaque(factor):
+def test_mode_RGBA_opaque(factor) -> None:
im = get_image("RGBA")
# With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255))
@@ -248,27 +252,27 @@ def test_mode_RGBA_opaque(factor):
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_RGBa(factor):
+def test_mode_RGBa(factor) -> None:
im = get_image("RGBa")
compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_I(factor):
+def test_mode_I(factor) -> None:
im = get_image("I")
compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_F(factor):
+def test_mode_F(factor) -> None:
im = get_image("F")
compare_reduce_with_reference(im, factor, 0, 0)
compare_reduce_with_box(im, factor)
@skip_unless_feature("jpg_2000")
-def test_jpeg2k():
+def test_jpeg2k() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
assert im.reduce(2).size == (320, 240)
diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py
index be49955dd..f4c9eb0e6 100644
--- a/Tests/test_image_resample.py
+++ b/Tests/test_image_resample.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from contextlib import contextmanager
import pytest
@@ -14,7 +16,7 @@ from .helper import (
class TestImagingResampleVulnerability:
# see https://github.com/python-pillow/Pillow/issues/1710
- def test_overflow(self):
+ def test_overflow(self) -> None:
im = hopper("L")
size_too_large = 0x100000008 // 4
size_normal = 1000 # unimportant
@@ -26,7 +28,7 @@ class TestImagingResampleVulnerability:
# any resampling filter will do here
im.im.resize((xsize, ysize), Image.Resampling.BILINEAR)
- def test_invalid_size(self):
+ def test_invalid_size(self) -> None:
im = hopper()
# Should not crash
@@ -38,7 +40,7 @@ class TestImagingResampleVulnerability:
with pytest.raises(ValueError):
im.resize((100, -100))
- def test_modify_after_resizing(self):
+ def test_modify_after_resizing(self) -> None:
im = hopper("RGB")
# get copy with same size
copy = im.resize(im.size)
@@ -81,7 +83,7 @@ class TestImagingCoreResampleAccuracy:
s_px[size[0] - x - 1, y] = 255 - val
return sample
- def check_case(self, case, sample):
+ def check_case(self, case, sample) -> None:
s_px = sample.load()
c_px = case.load()
for y in range(case.size[1]):
@@ -101,7 +103,7 @@ class TestImagingCoreResampleAccuracy:
)
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_reduce_box(self, mode):
+ def test_reduce_box(self, mode) -> None:
case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off
@@ -112,7 +114,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_reduce_bilinear(self, mode):
+ def test_reduce_bilinear(self, mode) -> None:
case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off
@@ -123,7 +125,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_reduce_hamming(self, mode):
+ def test_reduce_hamming(self, mode) -> None:
case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off
@@ -134,7 +136,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_reduce_bicubic(self, mode):
+ def test_reduce_bicubic(self, mode) -> None:
case = self.make_case(mode, (12, 12), 0xE1)
case = case.resize((6, 6), Image.Resampling.BICUBIC)
# fmt: off
@@ -146,7 +148,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (6, 6)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_reduce_lanczos(self, mode):
+ def test_reduce_lanczos(self, mode) -> None:
case = self.make_case(mode, (16, 16), 0xE1)
case = case.resize((8, 8), Image.Resampling.LANCZOS)
# fmt: off
@@ -159,7 +161,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (8, 8)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_enlarge_box(self, mode):
+ def test_enlarge_box(self, mode) -> None:
case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off
@@ -170,7 +172,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_enlarge_bilinear(self, mode):
+ def test_enlarge_bilinear(self, mode) -> None:
case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off
@@ -181,7 +183,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_enlarge_hamming(self, mode):
+ def test_enlarge_hamming(self, mode) -> None:
case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off
@@ -192,7 +194,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_enlarge_bicubic(self, mode):
+ def test_enlarge_bicubic(self, mode) -> None:
case = self.make_case(mode, (4, 4), 0xE1)
case = case.resize((8, 8), Image.Resampling.BICUBIC)
# fmt: off
@@ -205,7 +207,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (8, 8)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_enlarge_lanczos(self, mode):
+ def test_enlarge_lanczos(self, mode) -> None:
case = self.make_case(mode, (6, 6), 0xE1)
case = case.resize((12, 12), Image.Resampling.LANCZOS)
data = (
@@ -219,7 +221,7 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split():
self.check_case(channel, self.make_sample(data, (12, 12)))
- def test_box_filter_correct_range(self):
+ def test_box_filter_correct_range(self) -> None:
im = Image.new("RGB", (8, 8), "#1688ff").resize(
(100, 100), Image.Resampling.BOX
)
@@ -232,7 +234,7 @@ class TestCoreResampleConsistency:
im = Image.new(mode, (512, 9), fill)
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
- def run_case(self, case):
+ def run_case(self, case) -> None:
channel, color = case
px = channel.load()
for x in range(channel.size[0]):
@@ -241,7 +243,7 @@ class TestCoreResampleConsistency:
message = f"{px[x, y]} != {color} for pixel {(x, y)}"
assert px[x, y] == color, message
- def test_8u(self):
+ def test_8u(self) -> None:
im, color = self.make_case("RGB", (0, 64, 255))
r, g, b = im.split()
self.run_case((r, color[0]))
@@ -249,13 +251,13 @@ class TestCoreResampleConsistency:
self.run_case((b, color[2]))
self.run_case(self.make_case("L", 12))
- def test_32i(self):
+ def test_32i(self) -> None:
self.run_case(self.make_case("I", 12))
self.run_case(self.make_case("I", 0x7FFFFFFF))
self.run_case(self.make_case("I", -12))
self.run_case(self.make_case("I", -1 << 31))
- def test_32f(self):
+ def test_32f(self) -> None:
self.run_case(self.make_case("F", 1))
self.run_case(self.make_case("F", 3.40282306074e38))
self.run_case(self.make_case("F", 1.175494e-38))
@@ -273,7 +275,7 @@ class TestCoreResampleAlphaCorrect:
px[x, y] = tuple(pix)
return i
- def run_levels_case(self, i):
+ def run_levels_case(self, i) -> None:
px = i.load()
for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])}
@@ -283,7 +285,7 @@ class TestCoreResampleAlphaCorrect:
)
@pytest.mark.xfail(reason="Current implementation isn't precise enough")
- def test_levels_rgba(self):
+ def test_levels_rgba(self) -> None:
case = self.make_levels_case("RGBA")
self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX))
self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR))
@@ -292,7 +294,7 @@ class TestCoreResampleAlphaCorrect:
self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS))
@pytest.mark.xfail(reason="Current implementation isn't precise enough")
- def test_levels_la(self):
+ def test_levels_la(self) -> None:
case = self.make_levels_case("LA")
self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX))
self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR))
@@ -310,7 +312,7 @@ class TestCoreResampleAlphaCorrect:
px[x + xdiv4, y + ydiv4] = clean_pixel
return i
- def run_dirty_case(self, i, clean_pixel):
+ def run_dirty_case(self, i, clean_pixel) -> None:
px = i.load()
for y in range(i.size[1]):
for x in range(i.size[0]):
@@ -321,7 +323,7 @@ class TestCoreResampleAlphaCorrect:
)
assert px[x, y][:3] == clean_pixel, message
- def test_dirty_pixels_rgba(self):
+ def test_dirty_pixels_rgba(self) -> None:
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))
self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0))
self.run_dirty_case(
@@ -337,7 +339,7 @@ class TestCoreResampleAlphaCorrect:
case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0)
)
- def test_dirty_pixels_la(self):
+ def test_dirty_pixels_la(self) -> None:
case = self.make_dirty_case("LA", (255, 128), (0, 0))
self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,))
self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,))
@@ -353,22 +355,22 @@ class TestCoreResamplePasses:
yield
assert Image.core.get_stats()["new_count"] - count == diff
- def test_horizontal(self):
+ def test_horizontal(self) -> None:
im = hopper("L")
with self.count(1):
im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR)
- def test_vertical(self):
+ def test_vertical(self) -> None:
im = hopper("L")
with self.count(1):
im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR)
- def test_both(self):
+ def test_both(self) -> None:
im = hopper("L")
with self.count(2):
im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR)
- def test_box_horizontal(self):
+ def test_box_horizontal(self) -> None:
im = hopper("L")
box = (20, 0, im.size[0] - 20, im.size[1])
with self.count(1):
@@ -378,7 +380,7 @@ class TestCoreResamplePasses:
cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR)
assert_image_similar(with_box, cropped, 0.1)
- def test_box_vertical(self):
+ def test_box_vertical(self) -> None:
im = hopper("L")
box = (0, 20, im.size[0], im.size[1] - 20)
with self.count(1):
@@ -390,7 +392,7 @@ class TestCoreResamplePasses:
class TestCoreResampleCoefficients:
- def test_reduce(self):
+ def test_reduce(self) -> None:
test_color = 254
for size in range(400000, 400010, 2):
@@ -402,7 +404,7 @@ class TestCoreResampleCoefficients:
if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0]
- def test_nonzero_coefficients(self):
+ def test_non_zero_coefficients(self) -> None:
# regression test for the wrong coefficients calculation
# due to bug https://github.com/python-pillow/Pillow/issues/2161
im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF))
@@ -430,7 +432,7 @@ class TestCoreResampleBox:
Image.Resampling.LANCZOS,
),
)
- def test_wrong_arguments(self, resample):
+ def test_wrong_arguments(self, resample) -> None:
im = hopper()
im.resize((32, 32), resample, (0, 0, im.width, im.height))
im.resize((32, 32), resample, (20, 20, im.width, im.height))
@@ -476,7 +478,7 @@ class TestCoreResampleBox:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_tiles(self):
+ def test_tiles(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
assert im.size == (480, 360)
dst_size = (251, 188)
@@ -489,7 +491,7 @@ class TestCoreResampleBox:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
- def test_subsample(self):
+ def test_subsample(self) -> None:
# This test shows advantages of the subpixel resizing
# after supersampling (e.g. during JPEG decoding).
with Image.open("Tests/images/flower.jpg") as im:
@@ -516,14 +518,14 @@ class TestCoreResampleBox:
@pytest.mark.parametrize(
"resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR)
)
- def test_formats(self, mode, resample):
+ def test_formats(self, mode, resample) -> None:
im = hopper(mode)
box = (20, 20, im.size[0] - 20, im.size[1] - 20)
with_box = im.resize((32, 32), resample, box)
cropped = im.crop(box).resize((32, 32), resample)
assert_image_similar(cropped, with_box, 0.4)
- def test_passthrough(self):
+ def test_passthrough(self) -> None:
# When no resize is required
im = hopper()
@@ -537,7 +539,7 @@ class TestCoreResampleBox:
assert res.size == size
assert_image_equal(res, im.crop(box), f">>> {size} {box}")
- def test_no_passthrough(self):
+ def test_no_passthrough(self) -> None:
# When resize is required
im = hopper()
@@ -556,7 +558,7 @@ class TestCoreResampleBox:
@pytest.mark.parametrize(
"flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
)
- def test_skip_horizontal(self, flt):
+ def test_skip_horizontal(self, flt) -> None:
# Can skip resize for one dimension
im = hopper()
@@ -579,7 +581,7 @@ class TestCoreResampleBox:
@pytest.mark.parametrize(
"flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
)
- def test_skip_vertical(self, flt):
+ def test_skip_vertical(self, flt) -> None:
# Can skip resize for one dimension
im = hopper()
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index 83c54cf62..bd45ee893 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -1,7 +1,11 @@
"""
Tests for resize functionality.
"""
+from __future__ import annotations
+
from itertools import permutations
+from pathlib import Path
+from typing import Generator
import pytest
@@ -17,7 +21,9 @@ from .helper import (
class TestImagingCoreResize:
- def resize(self, im, size, f):
+ def resize(
+ self, im: Image.Image, size: tuple[int, int], f: Image.Resampling
+ ) -> Image.Image:
# Image class independent version of resize.
im.load()
return im._new(im.im.resize(size, f))
@@ -25,14 +31,14 @@ class TestImagingCoreResize:
@pytest.mark.parametrize(
"mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16")
)
- def test_nearest_mode(self, mode):
+ def test_nearest_mode(self, mode: str) -> None:
im = hopper(mode)
r = self.resize(im, (15, 12), Image.Resampling.NEAREST)
assert r.mode == mode
assert r.size == (15, 12)
assert r.im.bands == im.im.bands
- def test_convolution_modes(self):
+ def test_convolution_modes(self) -> None:
with pytest.raises(ValueError):
self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR)
with pytest.raises(ValueError):
@@ -57,7 +63,7 @@ class TestImagingCoreResize:
Image.Resampling.LANCZOS,
),
)
- def test_reduce_filters(self, resample):
+ def test_reduce_filters(self, resample: Image.Resampling) -> None:
r = self.resize(hopper("RGB"), (15, 12), resample)
assert r.mode == "RGB"
assert r.size == (15, 12)
@@ -73,7 +79,7 @@ class TestImagingCoreResize:
Image.Resampling.LANCZOS,
),
)
- def test_enlarge_filters(self, resample):
+ def test_enlarge_filters(self, resample: Image.Resampling) -> None:
r = self.resize(hopper("RGB"), (212, 195), resample)
assert r.mode == "RGB"
assert r.size == (212, 195)
@@ -97,7 +103,9 @@ class TestImagingCoreResize:
("LA", ("filled", "dirty")),
),
)
- def test_endianness(self, resample, mode, channels_set):
+ def test_endianness(
+ self, resample: Image.Resampling, mode: str, channels_set: tuple[str, ...]
+ ) -> None:
# Make an image with one colored pixel, in one channel.
# When resized, that channel should be the same as a GS image.
# Other channels should be unaffected.
@@ -137,17 +145,17 @@ class TestImagingCoreResize:
Image.Resampling.LANCZOS,
),
)
- def test_enlarge_zero(self, resample):
+ def test_enlarge_zero(self, resample: Image.Resampling) -> None:
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample)
assert r.mode == "RGB"
assert r.size == (212, 195)
assert r.getdata()[0] == (0, 0, 0)
- def test_unknown_filter(self):
+ def test_unknown_filter(self) -> None:
with pytest.raises(ValueError):
self.resize(hopper(), (10, 10), 9)
- def test_cross_platform(self, tmp_path):
+ def test_cross_platform(self, tmp_path: Path) -> None:
# This test is intended for only check for consistent behaviour across
# platforms. So if a future Pillow change requires that the test file
# be updated, that is okay.
@@ -160,7 +168,7 @@ class TestImagingCoreResize:
@pytest.fixture
-def gradients_image():
+def gradients_image() -> Generator[Image.Image, None, None]:
with Image.open("Tests/images/radial_gradients.png") as im:
im.load()
try:
@@ -170,7 +178,7 @@ def gradients_image():
class TestReducingGapResize:
- def test_reducing_gap_values(self, gradients_image):
+ def test_reducing_gap_values(self, gradients_image: Image.Image) -> None:
ref = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, reducing_gap=None
)
@@ -189,13 +197,18 @@ class TestReducingGapResize:
"box, epsilon",
((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)),
)
- def test_reducing_gap_1(self, gradients_image, box, epsilon):
+ def test_reducing_gap_1(
+ self,
+ gradients_image: Image.Image,
+ box: tuple[float, float, float, float],
+ epsilon: float,
+ ) -> None:
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
)
- with pytest.raises(AssertionError):
+ with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im)
assert_image_similar(ref, im, epsilon)
@@ -204,13 +217,18 @@ class TestReducingGapResize:
"box, epsilon",
((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)),
)
- def test_reducing_gap_2(self, gradients_image, box, epsilon):
+ def test_reducing_gap_2(
+ self,
+ gradients_image: Image.Image,
+ box: tuple[float, float, float, float],
+ epsilon: float,
+ ) -> None:
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
)
- with pytest.raises(AssertionError):
+ with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im)
assert_image_similar(ref, im, epsilon)
@@ -219,19 +237,26 @@ class TestReducingGapResize:
"box, epsilon",
((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)),
)
- def test_reducing_gap_3(self, gradients_image, box, epsilon):
+ def test_reducing_gap_3(
+ self,
+ gradients_image: Image.Image,
+ box: tuple[float, float, float, float],
+ epsilon: float,
+ ) -> None:
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
)
- with pytest.raises(AssertionError):
+ with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im)
assert_image_similar(ref, im, epsilon)
@pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)))
- def test_reducing_gap_8(self, gradients_image, box):
+ def test_reducing_gap_8(
+ self, gradients_image: Image.Image, box: tuple[float, float, float, float]
+ ) -> None:
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0
@@ -243,7 +268,12 @@ class TestReducingGapResize:
"box, epsilon",
(((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)),
)
- def test_box_filter(self, gradients_image, box, epsilon):
+ def test_box_filter(
+ self,
+ gradients_image: Image.Image,
+ box: tuple[float, float, float, float],
+ epsilon: float,
+ ) -> None:
ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box)
im = gradients_image.resize(
(52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0
@@ -253,8 +283,8 @@ class TestReducingGapResize:
class TestImageResize:
- def test_resize(self):
- def resize(mode, size):
+ def test_resize(self) -> None:
+ def resize(mode: str, size: tuple[int, int]) -> None:
out = hopper(mode).resize(size)
assert out.mode == mode
assert out.size == size
@@ -269,7 +299,7 @@ class TestImageResize:
im.resize((10, 10), "unknown")
@skip_unless_feature("libtiff")
- def test_load_first(self):
+ def test_load_first(self) -> None:
# load() may change the size of the image
# Test that resize() is calling it before getting the size
with Image.open("Tests/images/g4_orientation_5.tif") as im:
@@ -277,13 +307,13 @@ class TestImageResize:
assert im.size == (64, 64)
@pytest.mark.parametrize("mode", ("L", "RGB", "I", "F"))
- def test_default_filter_bicubic(self, mode):
+ def test_default_filter_bicubic(self, mode: str) -> None:
im = hopper(mode)
assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20))
@pytest.mark.parametrize(
"mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16")
)
- def test_default_filter_nearest(self, mode):
+ def test_default_filter_nearest(self, mode: str) -> None:
im = hopper(mode)
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))
diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py
index 82fe46b7d..51e0f5854 100644
--- a/Tests/test_image_rotate.py
+++ b/Tests/test_image_rotate.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -10,7 +12,7 @@ from .helper import (
)
-def rotate(im, mode, angle, center=None, translate=None):
+def rotate(im, mode, angle, center=None, translate=None) -> None:
out = im.rotate(angle, center=center, translate=translate)
assert out.mode == mode
assert out.size == im.size # default rotate clips output
@@ -25,13 +27,13 @@ def rotate(im, mode, angle, center=None, translate=None):
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
-def test_mode(mode):
+def test_mode(mode) -> None:
im = hopper(mode)
rotate(im, mode, 45)
@pytest.mark.parametrize("angle", (0, 90, 180, 270))
-def test_angle(angle):
+def test_angle(angle) -> None:
with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle)
@@ -40,12 +42,12 @@ def test_angle(angle):
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
-def test_zero(angle):
+def test_zero(angle) -> None:
im = Image.new("RGB", (0, 0))
rotate(im, im.mode, angle)
-def test_resample():
+def test_resample() -> None:
# Target image creation, inspected by eye.
# >>> im = Image.open('Tests/images/hopper.ppm')
# >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True)
@@ -62,7 +64,7 @@ def test_resample():
assert_image_similar(im, target, epsilon)
-def test_center_0():
+def test_center_0() -> None:
im = hopper()
im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC)
@@ -73,7 +75,7 @@ def test_center_0():
assert_image_similar(im, target, 15)
-def test_center_14():
+def test_center_14() -> None:
im = hopper()
im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC)
@@ -84,7 +86,7 @@ def test_center_14():
assert_image_similar(im, target, 10)
-def test_translate():
+def test_translate() -> None:
im = hopper()
with Image.open("Tests/images/hopper_45.png") as target:
target_origin = (target.size[1] / 2 - 64) - 5
@@ -97,7 +99,7 @@ def test_translate():
assert_image_similar(im, target, 1)
-def test_fastpath_center():
+def test_fastpath_center() -> None:
# if the center is -1,-1 and we rotate by 90<=x<=270 the
# resulting image should be black
for angle in (90, 180, 270):
@@ -105,7 +107,7 @@ def test_fastpath_center():
assert_image_equal(im, Image.new("RGB", im.size, "black"))
-def test_fastpath_translate():
+def test_fastpath_translate() -> None:
# if we post-translate by -128
# resulting image should be black
for angle in (0, 90, 180, 270):
@@ -113,26 +115,26 @@ def test_fastpath_translate():
assert_image_equal(im, Image.new("RGB", im.size, "black"))
-def test_center():
+def test_center() -> None:
im = hopper()
rotate(im, im.mode, 45, center=(0, 0))
rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0))
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0))
-def test_rotate_no_fill():
+def test_rotate_no_fill() -> None:
im = Image.new("RGB", (100, 100), "green")
im = im.rotate(45)
assert_image_equal_tofile(im, "Tests/images/rotate_45_no_fill.png")
-def test_rotate_with_fill():
+def test_rotate_with_fill() -> None:
im = Image.new("RGB", (100, 100), "green")
im = im.rotate(45, fillcolor="white")
assert_image_equal_tofile(im, "Tests/images/rotate_45_with_fill.png")
-def test_alpha_rotate_no_fill():
+def test_alpha_rotate_no_fill() -> None:
# Alpha images are handled differently internally
im = Image.new("RGBA", (10, 10), "green")
im = im.rotate(45, expand=1)
@@ -140,7 +142,7 @@ def test_alpha_rotate_no_fill():
assert corner == (0, 0, 0, 0)
-def test_alpha_rotate_with_fill():
+def test_alpha_rotate_with_fill() -> None:
# Alpha images are handled differently internally
im = Image.new("RGBA", (10, 10), "green")
im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255))
diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py
index 5cb7c9a8b..3385f81f5 100644
--- a/Tests/test_image_split.py
+++ b/Tests/test_image_split.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, features
@@ -5,8 +9,8 @@ from PIL import Image, features
from .helper import assert_image_equal, hopper
-def test_split():
- def split(mode):
+def test_split() -> None:
+ def split(mode: str) -> list[tuple[str, int, int]]:
layers = hopper(mode).split()
return [(i.mode, i.size[0], i.size[1]) for i in layers]
@@ -34,18 +38,18 @@ def test_split():
@pytest.mark.parametrize(
"mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr")
)
-def test_split_merge(mode):
+def test_split_merge(mode: str) -> None:
expected = Image.merge(mode, hopper(mode).split())
assert_image_equal(hopper(mode), expected)
-def test_split_open(tmp_path):
+def test_split_open(tmp_path: Path) -> None:
if features.check("zlib"):
test_file = str(tmp_path / "temp.png")
else:
test_file = str(tmp_path / "temp.pcx")
- def split_open(mode):
+ def split_open(mode: str) -> int:
hopper(mode).save(test_file)
with Image.open(test_file) as im:
return len(im.split())
diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py
index 4fd07a2b4..6aeeea2ed 100644
--- a/Tests/test_image_thumbnail.py
+++ b/Tests/test_image_thumbnail.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -12,14 +14,14 @@ from .helper import (
)
-def test_sanity():
+def test_sanity() -> None:
im = hopper()
assert im.thumbnail((100, 100)) is None
assert im.size == (100, 100)
-def test_aspect():
+def test_aspect() -> None:
im = Image.new("L", (128, 128))
im.thumbnail((100, 100))
assert im.size == (100, 100)
@@ -65,19 +67,19 @@ def test_aspect():
assert im.size == (75, 23) # ratio is 3.260869565217
-def test_division_by_zero():
+def test_division_by_zero() -> None:
im = Image.new("L", (200, 2))
im.thumbnail((75, 75))
assert im.size == (75, 1)
-def test_float():
+def test_float() -> None:
im = Image.new("L", (128, 128))
im.thumbnail((99.9, 99.9))
assert im.size == (99, 99)
-def test_no_resize():
+def test_no_resize() -> None:
# Check that draft() can resize the image to the destination size
with Image.open("Tests/images/hopper.jpg") as im:
im.draft(None, (64, 64))
@@ -90,7 +92,7 @@ def test_no_resize():
@skip_unless_feature("libtiff")
-def test_load_first():
+def test_load_first() -> None:
# load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations
with Image.open("Tests/images/g4_orientation_5.tif") as im:
@@ -104,7 +106,7 @@ def test_load_first():
assert im.size == (590, 88)
-def test_load_first_unless_jpeg():
+def test_load_first_unless_jpeg() -> None:
# Test that thumbnail() still uses draft() for JPEG
with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft
@@ -122,7 +124,7 @@ def test_load_first_unless_jpeg():
# valgrind test is failing with memory allocated in libjpeg
@pytest.mark.valgrind_known_error(reason="Known Failing")
-def test_DCT_scaling_edges():
+def test_DCT_scaling_edges() -> None:
# Make an image with red borders and size (N * 8) + 1 to cross DCT grid
im = Image.new("RGB", (257, 257), "red")
im.paste(Image.new("RGB", (235, 235)), (11, 11))
@@ -136,7 +138,7 @@ def test_DCT_scaling_edges():
assert_image_similar(thumb, ref, 1.5)
-def test_reducing_gap_values():
+def test_reducing_gap_values() -> None:
im = hopper()
im.thumbnail((18, 18), Image.Resampling.BICUBIC)
@@ -147,13 +149,13 @@ def test_reducing_gap_values():
ref = hopper()
ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
- with pytest.raises(AssertionError):
+ with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im)
assert_image_similar(ref, im, 3.5)
-def test_reducing_gap_for_DCT_scaling():
+def test_reducing_gap_for_DCT_scaling() -> None:
with Image.open("Tests/images/hopper.jpg") as ref:
# thumbnail should call draft with reducing_gap scale
ref.draft(None, (18 * 3, 18 * 3))
diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py
index a12ce329f..f7a3cc41d 100644
--- a/Tests/test_image_tobitmap.py
+++ b/Tests/test_image_tobitmap.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
import pytest
from .helper import assert_image_equal, fromstring, hopper
-def test_sanity():
+def test_sanity() -> None:
with pytest.raises(ValueError):
hopper().tobitmap()
diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py
index 31e1c0080..d32b6c09b 100644
--- a/Tests/test_image_tobytes.py
+++ b/Tests/test_image_tobytes.py
@@ -1,6 +1,8 @@
+from __future__ import annotations
+
from .helper import hopper
-def test_sanity():
+def test_sanity() -> None:
data = hopper().tobytes()
assert isinstance(data, bytes)
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index 64a5c9459..1067dd563 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import math
import pytest
@@ -8,21 +10,28 @@ from .helper import assert_image_equal, assert_image_similar, hopper
class TestImageTransform:
- def test_sanity(self):
- im = Image.new("L", (100, 100))
+ def test_sanity(self) -> None:
+ im = hopper()
- seq = tuple(range(10))
+ for transform in (
+ ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)),
+ ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)),
+ ImageTransform.ExtentTransform((0, 0) + im.size),
+ ImageTransform.QuadTransform(
+ (0, 0, 0, im.height, im.width, im.height, im.width, 0)
+ ),
+ ImageTransform.MeshTransform(
+ [
+ (
+ (0, 0) + im.size,
+ (0, 0, 0, im.height, im.width, im.height, im.width, 0),
+ )
+ ]
+ ),
+ ):
+ assert_image_equal(im, im.transform(im.size, transform))
- transform = ImageTransform.AffineTransform(seq[:6])
- im.transform((100, 100), transform)
- transform = ImageTransform.ExtentTransform(seq[:4])
- im.transform((100, 100), transform)
- transform = ImageTransform.QuadTransform(seq[:8])
- im.transform((100, 100), transform)
- transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])])
- im.transform((100, 100), transform)
-
- def test_info(self):
+ def test_info(self) -> None:
comment = b"File written by Adobe Photoshop\xa8 4.0"
with Image.open("Tests/images/hopper.gif") as im:
@@ -32,14 +41,14 @@ class TestImageTransform:
new_im = im.transform((100, 100), transform)
assert new_im.info["comment"] == comment
- def test_palette(self):
+ def test_palette(self) -> None:
with Image.open("Tests/images/hopper.gif") as im:
transformed = im.transform(
im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0]
)
assert im.palette.palette == transformed.palette.palette
- def test_extent(self):
+ def test_extent(self) -> None:
im = hopper("RGB")
(w, h) = im.size
transformed = im.transform(
@@ -54,7 +63,7 @@ class TestImageTransform:
# undone -- precision?
assert_image_similar(transformed, scaled, 23)
- def test_quad(self):
+ def test_quad(self) -> None:
# one simple quad transform, equivalent to scale & crop upper left quad
im = hopper("RGB")
(w, h) = im.size
@@ -82,7 +91,7 @@ class TestImageTransform:
("LA", (76, 0)),
),
)
- def test_fill(self, mode, expected_pixel):
+ def test_fill(self, mode, expected_pixel) -> None:
im = hopper(mode)
(w, h) = im.size
transformed = im.transform(
@@ -94,7 +103,7 @@ class TestImageTransform:
)
assert transformed.getpixel((w - 1, h - 1)) == expected_pixel
- def test_mesh(self):
+ def test_mesh(self) -> None:
# this should be a checkerboard of halfsized hoppers in ul, lr
im = hopper("RGBA")
(w, h) = im.size
@@ -133,7 +142,7 @@ class TestImageTransform:
assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2)))
assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h)))
- def _test_alpha_premult(self, op):
+ def _test_alpha_premult(self, op) -> None:
# create image with half white, half black,
# with the black half transparent.
# do op,
@@ -149,13 +158,13 @@ class TestImageTransform:
hist = im_background.histogram()
assert 40 * 10 == hist[-1]
- def test_alpha_premult_resize(self):
+ def test_alpha_premult_resize(self) -> None:
def op(im, sz):
return im.resize(sz, Image.Resampling.BILINEAR)
self._test_alpha_premult(op)
- def test_alpha_premult_transform(self):
+ def test_alpha_premult_transform(self) -> None:
def op(im, sz):
(w, h) = im.size
return im.transform(
@@ -164,7 +173,7 @@ class TestImageTransform:
self._test_alpha_premult(op)
- def _test_nearest(self, op, mode):
+ def _test_nearest(self, op, mode) -> None:
# create white image with half transparent,
# do op,
# the image should remain white with half transparent
@@ -187,14 +196,14 @@ class TestImageTransform:
)
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
- def test_nearest_resize(self, mode):
+ def test_nearest_resize(self, mode) -> None:
def op(im, sz):
return im.resize(sz, Image.Resampling.NEAREST)
self._test_nearest(op, mode)
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
- def test_nearest_transform(self, mode):
+ def test_nearest_transform(self, mode) -> None:
def op(im, sz):
(w, h) = im.size
return im.transform(
@@ -203,7 +212,7 @@ class TestImageTransform:
self._test_nearest(op, mode)
- def test_blank_fill(self):
+ def test_blank_fill(self) -> None:
# attempting to hit
# https://github.com/python-pillow/Pillow/issues/254 reported
#
@@ -225,13 +234,13 @@ class TestImageTransform:
self.test_mesh()
- def test_missing_method_data(self):
+ def test_missing_method_data(self) -> None:
with hopper() as im:
with pytest.raises(ValueError):
im.transform((100, 100), None)
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
- def test_unknown_resampling_filter(self, resample):
+ def test_unknown_resampling_filter(self, resample) -> None:
with hopper() as im:
(w, h) = im.size
with pytest.raises(ValueError):
@@ -254,7 +263,7 @@ class TestImageTransformAffine:
(270, Image.Transpose.ROTATE_270),
),
)
- def test_rotate(self, deg, transpose):
+ def test_rotate(self, deg, transpose) -> None:
im = self._test_image()
angle = -math.radians(deg)
@@ -304,7 +313,7 @@ class TestImageTransformAffine:
(Image.Resampling.BICUBIC, 1),
),
)
- def test_resize(self, scale, epsilon_scale, resample, epsilon):
+ def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None:
im = self._test_image()
size_up = int(round(im.width * scale)), int(round(im.height * scale))
@@ -333,7 +342,7 @@ class TestImageTransformAffine:
(Image.Resampling.BICUBIC, 1),
),
)
- def test_translate(self, x, y, epsilon_scale, resample, epsilon):
+ def test_translate(self, x, y, epsilon_scale, resample, epsilon) -> None:
im = self._test_image()
size_up = int(round(im.width + x)), int(round(im.height + y))
diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py
index 877f439ca..d384d8141 100644
--- a/Tests/test_image_transpose.py
+++ b/Tests/test_image_transpose.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
import pytest
+from PIL import Image
from PIL.Image import Transpose
from . import helper
@@ -12,7 +15,7 @@ HOPPER = {
@pytest.mark.parametrize("mode", HOPPER)
-def test_flip_left_right(mode):
+def test_flip_left_right(mode: str) -> None:
im = HOPPER[mode]
out = im.transpose(Transpose.FLIP_LEFT_RIGHT)
assert out.mode == mode
@@ -26,7 +29,7 @@ def test_flip_left_right(mode):
@pytest.mark.parametrize("mode", HOPPER)
-def test_flip_top_bottom(mode):
+def test_flip_top_bottom(mode: str) -> None:
im = HOPPER[mode]
out = im.transpose(Transpose.FLIP_TOP_BOTTOM)
assert out.mode == mode
@@ -40,7 +43,7 @@ def test_flip_top_bottom(mode):
@pytest.mark.parametrize("mode", HOPPER)
-def test_rotate_90(mode):
+def test_rotate_90(mode: str) -> None:
im = HOPPER[mode]
out = im.transpose(Transpose.ROTATE_90)
assert out.mode == mode
@@ -54,7 +57,7 @@ def test_rotate_90(mode):
@pytest.mark.parametrize("mode", HOPPER)
-def test_rotate_180(mode):
+def test_rotate_180(mode: str) -> None:
im = HOPPER[mode]
out = im.transpose(Transpose.ROTATE_180)
assert out.mode == mode
@@ -68,7 +71,7 @@ def test_rotate_180(mode):
@pytest.mark.parametrize("mode", HOPPER)
-def test_rotate_270(mode):
+def test_rotate_270(mode: str) -> None:
im = HOPPER[mode]
out = im.transpose(Transpose.ROTATE_270)
assert out.mode == mode
@@ -82,7 +85,7 @@ def test_rotate_270(mode):
@pytest.mark.parametrize("mode", HOPPER)
-def test_transpose(mode):
+def test_transpose(mode: str) -> None:
im = HOPPER[mode]
out = im.transpose(Transpose.TRANSPOSE)
assert out.mode == mode
@@ -96,7 +99,7 @@ def test_transpose(mode):
@pytest.mark.parametrize("mode", HOPPER)
-def test_tranverse(mode):
+def test_tranverse(mode: str) -> None:
im = HOPPER[mode]
out = im.transpose(Transpose.TRANSVERSE)
assert out.mode == mode
@@ -110,10 +113,10 @@ def test_tranverse(mode):
@pytest.mark.parametrize("mode", HOPPER)
-def test_roundtrip(mode):
+def test_roundtrip(mode: str) -> None:
im = HOPPER[mode]
- def transpose(first, second):
+ def transpose(first: Transpose, second: Transpose) -> Image.Image:
return im.transpose(first).transpose(second)
assert_image_equal(
diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py
index d0fea3854..94f57e066 100644
--- a/Tests/test_imagechops.py
+++ b/Tests/test_imagechops.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from PIL import Image, ImageChops
from .helper import assert_image_equal, hopper
@@ -10,10 +12,10 @@ GREEN = (0, 255, 0)
ORANGE = (255, 128, 0)
WHITE = (255, 255, 255)
-GREY = 128
+GRAY = 128
-def test_sanity():
+def test_sanity() -> None:
im = hopper("L")
ImageChops.constant(im, 128)
@@ -46,7 +48,7 @@ def test_sanity():
ImageChops.offset(im, 10, 20)
-def test_add():
+def test_add() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
@@ -58,7 +60,7 @@ def test_add():
assert new.getpixel((50, 50)) == ORANGE
-def test_add_scale_offset():
+def test_add_scale_offset() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
@@ -70,7 +72,7 @@ def test_add_scale_offset():
assert new.getpixel((50, 50)) == (202, 151, 100)
-def test_add_clip():
+def test_add_clip() -> None:
# Arrange
im = hopper()
@@ -81,7 +83,7 @@ def test_add_clip():
assert new.getpixel((50, 50)) == (255, 255, 254)
-def test_add_modulo():
+def test_add_modulo() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
@@ -93,7 +95,7 @@ def test_add_modulo():
assert new.getpixel((50, 50)) == ORANGE
-def test_add_modulo_no_clip():
+def test_add_modulo_no_clip() -> None:
# Arrange
im = hopper()
@@ -104,7 +106,7 @@ def test_add_modulo_no_clip():
assert new.getpixel((50, 50)) == (224, 76, 254)
-def test_blend():
+def test_blend() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
@@ -116,20 +118,20 @@ def test_blend():
assert new.getpixel((50, 50)) == BROWN
-def test_constant():
+def test_constant() -> None:
# Arrange
im = Image.new("RGB", (20, 10))
# Act
- new = ImageChops.constant(im, GREY)
+ new = ImageChops.constant(im, GRAY)
# Assert
assert new.size == im.size
- assert new.getpixel((0, 0)) == GREY
- assert new.getpixel((19, 9)) == GREY
+ assert new.getpixel((0, 0)) == GRAY
+ assert new.getpixel((19, 9)) == GRAY
-def test_darker_image():
+def test_darker_image() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
@@ -140,7 +142,7 @@ def test_darker_image():
assert_image_equal(new, im2)
-def test_darker_pixel():
+def test_darker_pixel() -> None:
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
@@ -151,7 +153,7 @@ def test_darker_pixel():
assert new.getpixel((50, 50)) == (240, 166, 0)
-def test_difference():
+def test_difference() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1:
with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2:
@@ -162,7 +164,7 @@ def test_difference():
assert new.getbbox() == (25, 25, 76, 76)
-def test_difference_pixel():
+def test_difference_pixel() -> None:
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2:
@@ -173,7 +175,7 @@ def test_difference_pixel():
assert new.getpixel((50, 50)) == (240, 166, 128)
-def test_duplicate():
+def test_duplicate() -> None:
# Arrange
im = hopper()
@@ -184,7 +186,7 @@ def test_duplicate():
assert_image_equal(new, im)
-def test_invert():
+def test_invert() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im:
# Act
@@ -196,7 +198,7 @@ def test_invert():
assert new.getpixel((50, 50)) == CYAN
-def test_lighter_image():
+def test_lighter_image() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
@@ -207,7 +209,7 @@ def test_lighter_image():
assert_image_equal(new, im1)
-def test_lighter_pixel():
+def test_lighter_pixel() -> None:
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
@@ -218,7 +220,7 @@ def test_lighter_pixel():
assert new.getpixel((50, 50)) == (255, 255, 127)
-def test_multiply_black():
+def test_multiply_black() -> None:
"""If you multiply an image with a solid black image,
the result is black."""
# Arrange
@@ -232,7 +234,7 @@ def test_multiply_black():
assert_image_equal(new, black)
-def test_multiply_green():
+def test_multiply_green() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im:
green = Image.new("RGB", im.size, "green")
@@ -246,7 +248,7 @@ def test_multiply_green():
assert new.getpixel((50, 50)) == BLACK
-def test_multiply_white():
+def test_multiply_white() -> None:
"""If you multiply with a solid white image, the image is unaffected."""
# Arrange
im1 = hopper()
@@ -259,7 +261,7 @@ def test_multiply_white():
assert_image_equal(new, im1)
-def test_offset():
+def test_offset() -> None:
# Arrange
xoffset = 45
yoffset = 20
@@ -276,7 +278,7 @@ def test_offset():
assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset)
-def test_screen():
+def test_screen() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
@@ -288,7 +290,7 @@ def test_screen():
assert new.getpixel((50, 50)) == ORANGE
-def test_subtract():
+def test_subtract() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
@@ -301,7 +303,7 @@ def test_subtract():
assert new.getpixel((50, 52)) == BLACK
-def test_subtract_scale_offset():
+def test_subtract_scale_offset() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
@@ -313,7 +315,7 @@ def test_subtract_scale_offset():
assert new.getpixel((50, 50)) == (100, 202, 100)
-def test_subtract_clip():
+def test_subtract_clip() -> None:
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
@@ -324,7 +326,7 @@ def test_subtract_clip():
assert new.getpixel((50, 50)) == (0, 0, 127)
-def test_subtract_modulo():
+def test_subtract_modulo() -> None:
# Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
@@ -337,7 +339,7 @@ def test_subtract_modulo():
assert new.getpixel((50, 52)) == BLACK
-def test_subtract_modulo_no_clip():
+def test_subtract_modulo_no_clip() -> None:
# Arrange
im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
@@ -348,7 +350,7 @@ def test_subtract_modulo_no_clip():
assert new.getpixel((50, 50)) == (241, 167, 127)
-def test_soft_light():
+def test_soft_light() -> None:
# Arrange
with Image.open("Tests/images/hopper.png") as im1:
with Image.open("Tests/images/hopper-XYZ.png") as im2:
@@ -360,7 +362,7 @@ def test_soft_light():
assert new.getpixel((15, 100)) == (1, 1, 3)
-def test_hard_light():
+def test_hard_light() -> None:
# Arrange
with Image.open("Tests/images/hopper.png") as im1:
with Image.open("Tests/images/hopper-XYZ.png") as im2:
@@ -372,7 +374,7 @@ def test_hard_light():
assert new.getpixel((15, 100)) == (1, 1, 2)
-def test_overlay():
+def test_overlay() -> None:
# Arrange
with Image.open("Tests/images/hopper.png") as im1:
with Image.open("Tests/images/hopper-XYZ.png") as im2:
@@ -384,7 +386,7 @@ def test_overlay():
assert new.getpixel((15, 100)) == (1, 1, 2)
-def test_logical():
+def test_logical() -> None:
def table(op, a, b):
out = []
for x in (a, b):
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index 8efe063c1..7f6527155 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -1,8 +1,11 @@
+from __future__ import annotations
+
import datetime
import os
import re
import shutil
from io import BytesIO
+from pathlib import Path
import pytest
@@ -30,7 +33,7 @@ SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc"
HAVE_PROFILE = os.path.exists(SRGB)
-def setup_module():
+def setup_module() -> None:
try:
from PIL import ImageCms
@@ -40,16 +43,16 @@ def setup_module():
pytest.skip(str(v))
-def skip_missing():
+def skip_missing() -> None:
if not HAVE_PROFILE:
pytest.skip("SRGB profile not available")
-def test_sanity():
+def test_sanity() -> None:
# basic smoke test.
# this mostly follows the cms_test outline.
-
- v = ImageCms.versions() # should return four strings
+ with pytest.warns(DeprecationWarning):
+ v = ImageCms.versions() # should return four strings
assert v[0] == "1.0.0 pil"
assert list(map(type, v)) == [str, str, str, str]
@@ -89,7 +92,17 @@ def test_sanity():
hopper().point(t)
-def test_name():
+def test_flags() -> None:
+ assert ImageCms.Flags.NONE == 0
+ assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
+ assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
+
+ assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16)
+ assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255)
+ assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255)
+
+
+def test_name() -> None:
skip_missing()
# get profile information for file
assert (
@@ -98,7 +111,7 @@ def test_name():
)
-def test_info():
+def test_info() -> None:
skip_missing()
assert ImageCms.getProfileInfo(SRGB).splitlines() == [
"sRGB IEC61966-2-1 black scaled",
@@ -108,7 +121,7 @@ def test_info():
]
-def test_copyright():
+def test_copyright() -> None:
skip_missing()
assert (
ImageCms.getProfileCopyright(SRGB).strip()
@@ -116,12 +129,12 @@ def test_copyright():
)
-def test_manufacturer():
+def test_manufacturer() -> None:
skip_missing()
assert ImageCms.getProfileManufacturer(SRGB).strip() == ""
-def test_model():
+def test_model() -> None:
skip_missing()
assert (
ImageCms.getProfileModel(SRGB).strip()
@@ -129,14 +142,14 @@ def test_model():
)
-def test_description():
+def test_description() -> None:
skip_missing()
assert (
ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled"
)
-def test_intent():
+def test_intent() -> None:
skip_missing()
assert ImageCms.getDefaultIntent(SRGB) == 0
support = ImageCms.isIntentSupported(
@@ -145,7 +158,7 @@ def test_intent():
assert support == 1
-def test_profile_object():
+def test_profile_object() -> None:
# same, using profile object
p = ImageCms.createProfile("sRGB")
# assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)"
@@ -158,7 +171,7 @@ def test_profile_object():
assert support == 1
-def test_extensions():
+def test_extensions() -> None:
# extensions
with Image.open("Tests/images/rgb.jpg") as i:
@@ -169,7 +182,7 @@ def test_extensions():
)
-def test_exceptions():
+def test_exceptions() -> None:
# Test mode mismatch
psRGB = ImageCms.createProfile("sRGB")
pLab = ImageCms.createProfile("LAB")
@@ -195,17 +208,17 @@ def test_exceptions():
ImageCms.isIntentSupported(SRGB, None, None)
-def test_display_profile():
+def test_display_profile() -> None:
# try fetching the profile for the current display device
ImageCms.get_display_profile()
-def test_lab_color_profile():
+def test_lab_color_profile() -> None:
ImageCms.createProfile("LAB", 5000)
ImageCms.createProfile("LAB", 6500)
-def test_unsupported_color_space():
+def test_unsupported_color_space() -> None:
with pytest.raises(
ImageCms.PyCMSError,
match=re.escape(
@@ -215,7 +228,7 @@ def test_unsupported_color_space():
ImageCms.createProfile("unsupported")
-def test_invalid_color_temperature():
+def test_invalid_color_temperature() -> None:
with pytest.raises(
ImageCms.PyCMSError,
match='Color temperature must be numeric, "invalid" not valid',
@@ -224,7 +237,7 @@ def test_invalid_color_temperature():
@pytest.mark.parametrize("flag", ("my string", -1))
-def test_invalid_flag(flag):
+def test_invalid_flag(flag) -> None:
with hopper() as im:
with pytest.raises(
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
@@ -232,7 +245,7 @@ def test_invalid_flag(flag):
ImageCms.profileToProfile(im, "foo", "bar", flags=flag)
-def test_simple_lab():
+def test_simple_lab() -> None:
i = Image.new("RGB", (10, 10), (128, 128, 128))
psRGB = ImageCms.createProfile("sRGB")
@@ -256,7 +269,7 @@ def test_simple_lab():
assert list(b_data) == [128] * 100
-def test_lab_color():
+def test_lab_color() -> None:
psRGB = ImageCms.createProfile("sRGB")
pLab = ImageCms.createProfile("LAB")
t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB")
@@ -271,7 +284,7 @@ def test_lab_color():
assert_image_similar_tofile(i, "Tests/images/hopper.Lab.tif", 3.5)
-def test_lab_srgb():
+def test_lab_srgb() -> None:
psRGB = ImageCms.createProfile("sRGB")
pLab = ImageCms.createProfile("LAB")
t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
@@ -288,7 +301,7 @@ def test_lab_srgb():
assert "sRGB" in ImageCms.getProfileDescription(profile)
-def test_lab_roundtrip():
+def test_lab_roundtrip() -> None:
# check to see if we're at least internally consistent.
psRGB = ImageCms.createProfile("sRGB")
pLab = ImageCms.createProfile("LAB")
@@ -305,7 +318,7 @@ def test_lab_roundtrip():
assert_image_similar(hopper(), out, 2)
-def test_profile_tobytes():
+def test_profile_tobytes() -> None:
with Image.open("Tests/images/rgb.jpg") as i:
p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"]))
@@ -317,12 +330,12 @@ def test_profile_tobytes():
assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2)
-def test_extended_information():
+def test_extended_information() -> None:
skip_missing()
o = ImageCms.getOpenProfile(SRGB)
p = o.profile
- def assert_truncated_tuple_equal(tup1, tup2, digits=10):
+ def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None:
# Helper function to reduce precision of tuples of floats
# recursively and then check equality.
power = 10**digits
@@ -464,7 +477,7 @@ def test_extended_information():
assert p.xcolor_space == "RGB "
-def test_non_ascii_path(tmp_path):
+def test_non_ascii_path(tmp_path: Path) -> None:
skip_missing()
tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc"))
try:
@@ -477,7 +490,7 @@ def test_non_ascii_path(tmp_path):
assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB"
-def test_profile_typesafety():
+def test_profile_typesafety() -> None:
"""Profile init type safety
prepatch, these would segfault, postpatch they should emit a typeerror
@@ -489,7 +502,7 @@ def test_profile_typesafety():
ImageCms.ImageCmsProfile(1).tobytes()
-def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel):
+def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None:
def create_test_image():
# set up test image with something interesting in the tested aux channel.
# fmt: off
@@ -544,31 +557,31 @@ def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel):
assert_image_equal(source_image_aux, result_image_aux)
-def test_preserve_auxiliary_channels_rgba():
+def test_preserve_auxiliary_channels_rgba() -> None:
assert_aux_channel_preserved(
mode="RGBA", transform_in_place=False, preserved_channel="A"
)
-def test_preserve_auxiliary_channels_rgba_in_place():
+def test_preserve_auxiliary_channels_rgba_in_place() -> None:
assert_aux_channel_preserved(
mode="RGBA", transform_in_place=True, preserved_channel="A"
)
-def test_preserve_auxiliary_channels_rgbx():
+def test_preserve_auxiliary_channels_rgbx() -> None:
assert_aux_channel_preserved(
mode="RGBX", transform_in_place=False, preserved_channel="X"
)
-def test_preserve_auxiliary_channels_rgbx_in_place():
+def test_preserve_auxiliary_channels_rgbx_in_place() -> None:
assert_aux_channel_preserved(
mode="RGBX", transform_in_place=True, preserved_channel="X"
)
-def test_auxiliary_channels_isolated():
+def test_auxiliary_channels_isolated() -> None:
# test data in aux channels does not affect non-aux channels
aux_channel_formats = [
# format, profile, color-only format, source test image
@@ -618,7 +631,7 @@ def test_auxiliary_channels_isolated():
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
-def test_rgb_lab(mode):
+def test_rgb_lab(mode) -> None:
im = Image.new(mode, (1, 1))
converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
@@ -626,3 +639,12 @@ def test_rgb_lab(mode):
im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode)
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
+
+
+def test_deprecation() -> None:
+ with pytest.warns(DeprecationWarning):
+ assert ImageCms.DESCRIPTION.strip().startswith("pyCMS")
+ with pytest.warns(DeprecationWarning):
+ assert ImageCms.VERSION == "1.0.0 pil"
+ with pytest.warns(DeprecationWarning):
+ assert isinstance(ImageCms.FLAGS, dict)
diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py
index 2fae6151c..6eea7886d 100644
--- a/Tests/test_imagecolor.py
+++ b/Tests/test_imagecolor.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageColor
-def test_hash():
+def test_hash() -> None:
# short 3 components
assert (255, 0, 0) == ImageColor.getrgb("#f00")
assert (0, 255, 0) == ImageColor.getrgb("#0f0")
@@ -55,7 +57,7 @@ def test_hash():
ImageColor.getrgb("#f00000 ")
-def test_colormap():
+def test_colormap() -> None:
assert (0, 0, 0) == ImageColor.getrgb("black")
assert (255, 255, 255) == ImageColor.getrgb("white")
assert (255, 255, 255) == ImageColor.getrgb("WHITE")
@@ -64,7 +66,7 @@ def test_colormap():
ImageColor.getrgb("black ")
-def test_functions():
+def test_functions() -> None:
# rgb numbers
assert (255, 0, 0) == ImageColor.getrgb("rgb(255,0,0)")
assert (0, 255, 0) == ImageColor.getrgb("rgb(0,255,0)")
@@ -158,7 +160,7 @@ def test_functions():
# look for rounding errors (based on code by Tim Hatch)
-def test_rounding_errors():
+def test_rounding_errors() -> None:
for color in ImageColor.colormap:
expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0))
actual = ImageColor.getcolor(color, "L")
@@ -193,11 +195,11 @@ def test_rounding_errors():
Image.new("LA", (1, 1), "white")
-def test_color_hsv():
+def test_color_hsv() -> None:
assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV")
-def test_color_too_long():
+def test_color_too_long() -> None:
# Arrange
color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)"
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 4052c41ff..86d25b1eb 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import contextlib
import os.path
@@ -45,7 +47,7 @@ KITE_POINTS = (
)
-def test_sanity():
+def test_sanity() -> None:
im = hopper("RGB").copy()
draw = ImageDraw.ImageDraw(im)
@@ -57,13 +59,13 @@ def test_sanity():
draw.rectangle(list(range(4)))
-def test_valueerror():
+def test_valueerror() -> None:
with Image.open("Tests/images/chi.gif") as im:
draw = ImageDraw.Draw(im)
draw.line((0, 0), fill=(0, 0, 0))
-def test_mode_mismatch():
+def test_mode_mismatch() -> None:
im = hopper("RGB").copy()
with pytest.raises(ValueError):
@@ -72,7 +74,7 @@ def test_mode_mismatch():
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
-def test_arc(bbox, start, end):
+def test_arc(bbox, start, end) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -85,7 +87,7 @@ def test_arc(bbox, start, end):
@pytest.mark.parametrize("bbox", BBOX)
-def test_arc_end_le_start(bbox):
+def test_arc_end_le_start(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -100,7 +102,7 @@ def test_arc_end_le_start(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_arc_no_loops(bbox):
+def test_arc_no_loops(bbox) -> None:
# No need to go in loops
# Arrange
im = Image.new("RGB", (W, H))
@@ -116,7 +118,7 @@ def test_arc_no_loops(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_arc_width(bbox):
+def test_arc_width(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -129,7 +131,7 @@ def test_arc_width(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_arc_width_pieslice_large(bbox):
+def test_arc_width_pieslice_large(bbox) -> None:
# Tests an arc with a large enough width that it is a pieslice
# Arrange
im = Image.new("RGB", (W, H))
@@ -143,7 +145,7 @@ def test_arc_width_pieslice_large(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_arc_width_fill(bbox):
+def test_arc_width_fill(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -156,7 +158,7 @@ def test_arc_width_fill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_arc_width_non_whole_angle(bbox):
+def test_arc_width_non_whole_angle(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -169,7 +171,7 @@ def test_arc_width_non_whole_angle(bbox):
assert_image_similar_tofile(im, expected, 1)
-def test_arc_high():
+def test_arc_high() -> None:
# Arrange
im = Image.new("RGB", (200, 200))
draw = ImageDraw.Draw(im)
@@ -182,7 +184,7 @@ def test_arc_high():
assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_high.png")
-def test_bitmap():
+def test_bitmap() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -198,7 +200,7 @@ def test_bitmap():
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", BBOX)
-def test_chord(mode, bbox):
+def test_chord(mode, bbox) -> None:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@@ -212,7 +214,7 @@ def test_chord(mode, bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_chord_width(bbox):
+def test_chord_width(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -225,7 +227,7 @@ def test_chord_width(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_chord_width_fill(bbox):
+def test_chord_width_fill(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -238,7 +240,7 @@ def test_chord_width_fill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_chord_zero_width(bbox):
+def test_chord_zero_width(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -250,7 +252,7 @@ def test_chord_zero_width(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png")
-def test_chord_too_fat():
+def test_chord_too_fat() -> None:
# Arrange
im = Image.new("RGB", (100, 100))
draw = ImageDraw.Draw(im)
@@ -264,7 +266,7 @@ def test_chord_too_fat():
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse(mode, bbox):
+def test_ellipse(mode, bbox) -> None:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@@ -278,7 +280,7 @@ def test_ellipse(mode, bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse_translucent(bbox):
+def test_ellipse_translucent(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
@@ -291,7 +293,7 @@ def test_ellipse_translucent(bbox):
assert_image_similar_tofile(im, expected, 1)
-def test_ellipse_edge():
+def test_ellipse_edge() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -303,7 +305,7 @@ def test_ellipse_edge():
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1)
-def test_ellipse_symmetric():
+def test_ellipse_symmetric() -> None:
for width, bbox in (
(100, (24, 24, 75, 75)),
(101, (25, 25, 75, 75)),
@@ -315,7 +317,7 @@ def test_ellipse_symmetric():
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse_width(bbox):
+def test_ellipse_width(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -327,7 +329,7 @@ def test_ellipse_width(bbox):
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1)
-def test_ellipse_width_large():
+def test_ellipse_width_large() -> None:
# Arrange
im = Image.new("RGB", (500, 500))
draw = ImageDraw.Draw(im)
@@ -340,7 +342,7 @@ def test_ellipse_width_large():
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse_width_fill(bbox):
+def test_ellipse_width_fill(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -353,7 +355,7 @@ def test_ellipse_width_fill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse_zero_width(bbox):
+def test_ellipse_zero_width(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -392,13 +394,13 @@ def ellipse_various_sizes_helper(filled):
return im
-def test_ellipse_various_sizes():
+def test_ellipse_various_sizes() -> None:
im = ellipse_various_sizes_helper(False)
assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_various_sizes.png")
-def test_ellipse_various_sizes_filled():
+def test_ellipse_various_sizes_filled() -> None:
im = ellipse_various_sizes_helper(True)
assert_image_equal_tofile(
@@ -407,7 +409,7 @@ def test_ellipse_various_sizes_filled():
@pytest.mark.parametrize("points", POINTS)
-def test_line(points):
+def test_line(points) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -419,7 +421,7 @@ def test_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
-def test_shape1():
+def test_shape1() -> None:
# Arrange
im = Image.new("RGB", (100, 100), "white")
draw = ImageDraw.Draw(im)
@@ -440,7 +442,7 @@ def test_shape1():
assert_image_equal_tofile(im, "Tests/images/imagedraw_shape1.png")
-def test_shape2():
+def test_shape2() -> None:
# Arrange
im = Image.new("RGB", (100, 100), "white")
draw = ImageDraw.Draw(im)
@@ -461,7 +463,7 @@ def test_shape2():
assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png")
-def test_transform():
+def test_transform() -> None:
# Arrange
im = Image.new("RGB", (100, 100), "white")
expected = im.copy()
@@ -480,7 +482,7 @@ def test_transform():
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
-def test_pieslice(bbox, start, end):
+def test_pieslice(bbox, start, end) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -493,7 +495,7 @@ def test_pieslice(bbox, start, end):
@pytest.mark.parametrize("bbox", BBOX)
-def test_pieslice_width(bbox):
+def test_pieslice_width(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -506,7 +508,7 @@ def test_pieslice_width(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_pieslice_width_fill(bbox):
+def test_pieslice_width_fill(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -520,7 +522,7 @@ def test_pieslice_width_fill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_pieslice_zero_width(bbox):
+def test_pieslice_zero_width(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -532,7 +534,7 @@ def test_pieslice_zero_width(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png")
-def test_pieslice_wide():
+def test_pieslice_wide() -> None:
# Arrange
im = Image.new("RGB", (200, 100))
draw = ImageDraw.Draw(im)
@@ -544,7 +546,7 @@ def test_pieslice_wide():
assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png")
-def test_pieslice_no_spikes():
+def test_pieslice_no_spikes() -> None:
im = Image.new("RGB", (161, 161), "white")
draw = ImageDraw.Draw(im)
cxs = (
@@ -575,7 +577,7 @@ def test_pieslice_no_spikes():
@pytest.mark.parametrize("points", POINTS)
-def test_point(points):
+def test_point(points) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -587,7 +589,7 @@ def test_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
-def test_point_I16():
+def test_point_I16() -> None:
# Arrange
im = Image.new("I;16", (1, 1))
draw = ImageDraw.Draw(im)
@@ -600,7 +602,7 @@ def test_point_I16():
@pytest.mark.parametrize("points", POINTS)
-def test_polygon(points):
+def test_polygon(points) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -614,7 +616,7 @@ def test_polygon(points):
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("kite_points", KITE_POINTS)
-def test_polygon_kite(mode, kite_points):
+def test_polygon_kite(mode, kite_points) -> None:
# Test drawing lines of different gradients (dx>dy, dy>dx) and
# vertical (dx==0) and horizontal (dy==0) lines
# Arrange
@@ -629,7 +631,7 @@ def test_polygon_kite(mode, kite_points):
assert_image_equal_tofile(im, expected)
-def test_polygon_1px_high():
+def test_polygon_1px_high() -> None:
# Test drawing a 1px high polygon
# Arrange
im = Image.new("RGB", (3, 3))
@@ -643,7 +645,7 @@ def test_polygon_1px_high():
assert_image_equal_tofile(im, expected)
-def test_polygon_1px_high_translucent():
+def test_polygon_1px_high_translucent() -> None:
# Test drawing a translucent 1px high polygon
# Arrange
im = Image.new("RGB", (4, 3))
@@ -657,7 +659,7 @@ def test_polygon_1px_high_translucent():
assert_image_equal_tofile(im, expected)
-def test_polygon_translucent():
+def test_polygon_translucent() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
@@ -671,7 +673,7 @@ def test_polygon_translucent():
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle(bbox):
+def test_rectangle(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -683,7 +685,7 @@ def test_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png")
-def test_big_rectangle():
+def test_big_rectangle() -> None:
# Test drawing a rectangle bigger than the image
# Arrange
im = Image.new("RGB", (W, H))
@@ -698,7 +700,7 @@ def test_big_rectangle():
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_width(bbox):
+def test_rectangle_width(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -712,7 +714,7 @@ def test_rectangle_width(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_width_fill(bbox):
+def test_rectangle_width_fill(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -726,7 +728,7 @@ def test_rectangle_width_fill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_zero_width(bbox):
+def test_rectangle_zero_width(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -739,7 +741,7 @@ def test_rectangle_zero_width(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_I16(bbox):
+def test_rectangle_I16(bbox) -> None:
# Arrange
im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im)
@@ -752,7 +754,7 @@ def test_rectangle_I16(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_translucent_outline(bbox):
+def test_rectangle_translucent_outline(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
@@ -770,7 +772,7 @@ def test_rectangle_translucent_outline(bbox):
"xy",
[(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))],
)
-def test_rounded_rectangle(xy):
+def test_rounded_rectangle(xy) -> None:
# Arrange
im = Image.new("RGB", (200, 200))
draw = ImageDraw.Draw(im)
@@ -786,7 +788,9 @@ def test_rounded_rectangle(xy):
@pytest.mark.parametrize("top_right", (True, False))
@pytest.mark.parametrize("bottom_right", (True, False))
@pytest.mark.parametrize("bottom_left", (True, False))
-def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_left):
+def test_rounded_rectangle_corners(
+ top_left, top_right, bottom_right, bottom_left
+) -> None:
corners = (top_left, top_right, bottom_right, bottom_left)
# Arrange
@@ -820,7 +824,7 @@ def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_lef
((10, 20, 190, 181), 85, "height"),
],
)
-def test_rounded_rectangle_non_integer_radius(xy, radius, type):
+def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None:
# Arrange
im = Image.new("RGB", (200, 200))
draw = ImageDraw.Draw(im)
@@ -836,7 +840,7 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rounded_rectangle_zero_radius(bbox):
+def test_rounded_rectangle_zero_radius(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -858,7 +862,7 @@ def test_rounded_rectangle_zero_radius(bbox):
((20, 20, 80, 80), "both"),
],
)
-def test_rounded_rectangle_translucent(xy, suffix):
+def test_rounded_rectangle_translucent(xy, suffix) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
@@ -875,7 +879,7 @@ def test_rounded_rectangle_translucent(xy, suffix):
@pytest.mark.parametrize("bbox", BBOX)
-def test_floodfill(bbox):
+def test_floodfill(bbox) -> None:
red = ImageColor.getrgb("red")
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
@@ -908,7 +912,7 @@ def test_floodfill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_floodfill_border(bbox):
+def test_floodfill_border(bbox) -> None:
# floodfill() is experimental
# Arrange
@@ -930,7 +934,7 @@ def test_floodfill_border(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_floodfill_thresh(bbox):
+def test_floodfill_thresh(bbox) -> None:
# floodfill() is experimental
# Arrange
@@ -946,7 +950,7 @@ def test_floodfill_thresh(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png")
-def test_floodfill_not_negative():
+def test_floodfill_not_negative() -> None:
# floodfill() is experimental
# Test that floodfill does not extend into negative coordinates
@@ -974,7 +978,7 @@ def create_base_image_draw(
return img, ImageDraw.Draw(img)
-def test_square():
+def test_square() -> None:
expected = os.path.join(IMAGES_PATH, "square.png")
img, draw = create_base_image_draw((10, 10))
draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK)
@@ -987,7 +991,7 @@ def test_square():
assert_image_equal_tofile(img, expected, "square as normal rectangle failed")
-def test_triangle_right():
+def test_triangle_right() -> None:
img, draw = create_base_image_draw((20, 20))
draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK)
assert_image_equal_tofile(
@@ -999,7 +1003,7 @@ def test_triangle_right():
"fill, suffix",
((BLACK, "width"), (None, "width_no_fill")),
)
-def test_triangle_right_width(fill, suffix):
+def test_triangle_right_width(fill, suffix) -> None:
img, draw = create_base_image_draw((100, 100))
draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5)
assert_image_equal_tofile(
@@ -1007,7 +1011,7 @@ def test_triangle_right_width(fill, suffix):
)
-def test_line_horizontal():
+def test_line_horizontal() -> None:
img, draw = create_base_image_draw((20, 20))
draw.line((5, 5, 14, 5), BLACK, 2)
assert_image_equal_tofile(
@@ -1045,7 +1049,7 @@ def test_line_horizontal():
)
-def test_line_h_s1_w2():
+def test_line_h_s1_w2() -> None:
pytest.skip("failing")
img, draw = create_base_image_draw((20, 20))
draw.line((5, 5, 14, 6), BLACK, 2)
@@ -1056,7 +1060,7 @@ def test_line_h_s1_w2():
)
-def test_line_vertical():
+def test_line_vertical() -> None:
img, draw = create_base_image_draw((20, 20))
draw.line((5, 5, 5, 14), BLACK, 2)
assert_image_equal_tofile(
@@ -1102,7 +1106,7 @@ def test_line_vertical():
)
-def test_line_oblique_45():
+def test_line_oblique_45() -> None:
expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png")
img, draw = create_base_image_draw((20, 20))
draw.line((5, 5, 14, 14), BLACK, 3)
@@ -1124,7 +1128,7 @@ def test_line_oblique_45():
)
-def test_wide_line_dot():
+def test_wide_line_dot() -> None:
# Test drawing a wide "line" from one point to another just draws a single point
# Arrange
im = Image.new("RGB", (W, H))
@@ -1137,7 +1141,7 @@ def test_wide_line_dot():
assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1)
-def test_wide_line_larger_than_int():
+def test_wide_line_larger_than_int() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -1231,7 +1235,7 @@ def test_wide_line_larger_than_int():
],
],
)
-def test_line_joint(xy):
+def test_line_joint(xy) -> None:
im = Image.new("RGB", (500, 325))
draw = ImageDraw.Draw(im)
@@ -1242,7 +1246,7 @@ def test_line_joint(xy):
assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3)
-def test_textsize_empty_string():
+def test_textsize_empty_string() -> None:
# https://github.com/python-pillow/Pillow/issues/2783
# Arrange
im = Image.new("RGB", (W, H))
@@ -1258,7 +1262,7 @@ def test_textsize_empty_string():
@skip_unless_feature("freetype2")
-def test_textbbox_stroke():
+def test_textbbox_stroke() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -1272,7 +1276,7 @@ def test_textbbox_stroke():
@skip_unless_feature("freetype2")
-def test_stroke():
+def test_stroke() -> None:
for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items():
# Arrange
im = Image.new("RGB", (120, 130))
@@ -1289,7 +1293,7 @@ def test_stroke():
@skip_unless_feature("freetype2")
-def test_stroke_descender():
+def test_stroke_descender() -> None:
# Arrange
im = Image.new("RGB", (120, 130))
draw = ImageDraw.Draw(im)
@@ -1303,7 +1307,7 @@ def test_stroke_descender():
@skip_unless_feature("freetype2")
-def test_split_word():
+def test_split_word() -> None:
# Arrange
im = Image.new("RGB", (230, 55))
expected = im.copy()
@@ -1324,7 +1328,7 @@ def test_split_word():
@skip_unless_feature("freetype2")
-def test_stroke_multiline():
+def test_stroke_multiline() -> None:
# Arrange
im = Image.new("RGB", (100, 250))
draw = ImageDraw.Draw(im)
@@ -1340,7 +1344,7 @@ def test_stroke_multiline():
@skip_unless_feature("freetype2")
-def test_setting_default_font():
+def test_setting_default_font() -> None:
# Arrange
im = Image.new("RGB", (100, 250))
draw = ImageDraw.Draw(im)
@@ -1357,7 +1361,7 @@ def test_setting_default_font():
assert isinstance(draw.getfont(), ImageFont.load_default().__class__)
-def test_default_font_size():
+def test_default_font_size() -> None:
freetype_support = features.check_module("freetype2")
text = "Default font at a specific size."
@@ -1384,7 +1388,7 @@ def test_default_font_size():
@pytest.mark.parametrize("bbox", BBOX)
-def test_same_color_outline(bbox):
+def test_same_color_outline(bbox) -> None:
# Prepare shape
x0, y0 = 5, 5
x1, y1 = 5, 50
@@ -1430,7 +1434,7 @@ def test_same_color_outline(bbox):
(3, "triangle_width", {"width": 5, "outline": "yellow"}),
],
)
-def test_draw_regular_polygon(n_sides, polygon_name, args):
+def test_draw_regular_polygon(n_sides, polygon_name, args) -> None:
im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0))
filename = f"Tests/images/imagedraw_{polygon_name}.png"
draw = ImageDraw.Draw(im)
@@ -1467,7 +1471,7 @@ def test_draw_regular_polygon(n_sides, polygon_name, args):
),
],
)
-def test_compute_regular_polygon_vertices(n_sides, expected_vertices):
+def test_compute_regular_polygon_vertices(n_sides, expected_vertices) -> None:
bounding_circle = (W // 2, H // 2, 25)
vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0)
assert vertices == expected_vertices
@@ -1519,13 +1523,13 @@ def test_compute_regular_polygon_vertices(n_sides, expected_vertices):
)
def test_compute_regular_polygon_vertices_input_error_handling(
n_sides, bounding_circle, rotation, expected_error, error_message
-):
+) -> None:
with pytest.raises(expected_error) as e:
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
assert str(e.value) == error_message
-def test_continuous_horizontal_edges_polygon():
+def test_continuous_horizontal_edges_polygon() -> None:
xy = [
(2, 6),
(6, 6),
@@ -1544,7 +1548,7 @@ def test_continuous_horizontal_edges_polygon():
)
-def test_discontiguous_corners_polygon():
+def test_discontiguous_corners_polygon() -> None:
img, draw = create_base_image_draw((84, 68))
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
@@ -1556,7 +1560,7 @@ def test_discontiguous_corners_polygon():
assert_image_similar_tofile(img, expected, 1)
-def test_polygon2():
+def test_polygon2() -> None:
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")
@@ -1565,7 +1569,7 @@ def test_polygon2():
@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0)))
-def test_incorrectly_ordered_coordinates(xy):
+def test_incorrectly_ordered_coordinates(xy) -> None:
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
with pytest.raises(ValueError):
diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py
index a2c2fa1f0..07a25b84b 100644
--- a/Tests/test_imagedraw2.py
+++ b/Tests/test_imagedraw2.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import os.path
import pytest
@@ -41,7 +43,7 @@ POINTS = (
FONT_PATH = "Tests/fonts/FreeMono.ttf"
-def test_sanity():
+def test_sanity() -> None:
im = hopper("RGB").copy()
draw = ImageDraw2.Draw(im)
@@ -54,7 +56,7 @@ def test_sanity():
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse(bbox):
+def test_ellipse(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -68,7 +70,7 @@ def test_ellipse(bbox):
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1)
-def test_ellipse_edge():
+def test_ellipse_edge() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -82,7 +84,7 @@ def test_ellipse_edge():
@pytest.mark.parametrize("points", POINTS)
-def test_line(points):
+def test_line(points) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -96,7 +98,7 @@ def test_line(points):
@pytest.mark.parametrize("points", POINTS)
-def test_line_pen_as_brush(points):
+def test_line_pen_as_brush(points) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -112,7 +114,7 @@ def test_line_pen_as_brush(points):
@pytest.mark.parametrize("points", POINTS)
-def test_polygon(points):
+def test_polygon(points) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -127,7 +129,7 @@ def test_polygon(points):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle(bbox):
+def test_rectangle(bbox) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -141,7 +143,7 @@ def test_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png")
-def test_big_rectangle():
+def test_big_rectangle() -> None:
# Test drawing a rectangle bigger than the image
# Arrange
im = Image.new("RGB", (W, H))
@@ -158,7 +160,7 @@ def test_big_rectangle():
@skip_unless_feature("freetype2")
-def test_text():
+def test_text() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -173,7 +175,7 @@ def test_text():
@skip_unless_feature("freetype2")
-def test_textbbox():
+def test_textbbox() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -188,7 +190,7 @@ def test_textbbox():
@skip_unless_feature("freetype2")
-def test_textsize_empty_string():
+def test_textsize_empty_string() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -204,7 +206,7 @@ def test_textsize_empty_string():
@skip_unless_feature("freetype2")
-def test_flush():
+def test_flush() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py
index 221ef8cdb..9ce9cda82 100644
--- a/Tests/test_imageenhance.py
+++ b/Tests/test_imageenhance.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageEnhance
@@ -5,7 +7,7 @@ from PIL import Image, ImageEnhance
from .helper import assert_image_equal, hopper
-def test_sanity():
+def test_sanity() -> None:
# FIXME: assert_image
# Implicit asserts no exception:
ImageEnhance.Color(hopper()).enhance(0.5)
@@ -14,7 +16,7 @@ def test_sanity():
ImageEnhance.Sharpness(hopper()).enhance(0.5)
-def test_crash():
+def test_crash() -> None:
# crashes on small images
im = Image.new("RGB", (1, 1))
ImageEnhance.Sharpness(im).enhance(0.5)
@@ -32,7 +34,7 @@ def _half_transparent_image():
return im
-def _check_alpha(im, original, op, amount):
+def _check_alpha(im, original, op, amount) -> None:
assert im.getbands() == original.getbands()
assert_image_equal(
im.getchannel("A"),
@@ -42,7 +44,7 @@ def _check_alpha(im, original, op, amount):
@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
-def test_alpha(op):
+def test_alpha(op) -> None:
# Issue https://github.com/python-pillow/Pillow/issues/899
# Is alpha preserved through image enhancement?
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index ff75b8c2a..491409781 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -28,7 +30,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK
class TestImageFile:
- def test_parser(self):
+ def test_parser(self) -> None:
def roundtrip(format):
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
if format in ("MSP", "XBM"):
@@ -82,7 +84,7 @@ class TestImageFile:
with pytest.raises(OSError):
roundtrip("PDF")
- def test_ico(self):
+ def test_ico(self) -> None:
with open("Tests/images/python.ico", "rb") as f:
data = f.read()
with ImageFile.Parser() as p:
@@ -91,7 +93,7 @@ class TestImageFile:
@skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
- def test_incremental_webp(self):
+ def test_incremental_webp(self) -> None:
with ImageFile.Parser() as p:
with open("Tests/images/hopper.webp", "rb") as f:
p.feed(f.read(1024))
@@ -103,7 +105,7 @@ class TestImageFile:
assert (128, 128) == p.image.size
@skip_unless_feature("zlib")
- def test_safeblock(self):
+ def test_safeblock(self) -> None:
im1 = hopper()
try:
@@ -114,16 +116,17 @@ class TestImageFile:
assert_image_equal(im1, im2)
- def test_raise_oserror(self):
- with pytest.raises(OSError):
- ImageFile.raise_oserror(1)
+ def test_raise_oserror(self) -> None:
+ with pytest.warns(DeprecationWarning):
+ with pytest.raises(OSError):
+ ImageFile.raise_oserror(1)
- def test_raise_typeerror(self):
+ def test_raise_typeerror(self) -> None:
with pytest.raises(TypeError):
parser = ImageFile.Parser()
parser.feed(1)
- def test_negative_stride(self):
+ def test_negative_stride(self) -> None:
with open("Tests/images/raw_negative_stride.bin", "rb") as f:
input = f.read()
p = ImageFile.Parser()
@@ -131,11 +134,11 @@ class TestImageFile:
with pytest.raises(OSError):
p.close()
- def test_no_format(self):
+ def test_no_format(self) -> None:
buf = BytesIO(b"\x00" * 255)
class DummyImageFile(ImageFile.ImageFile):
- def _open(self):
+ def _open(self) -> None:
self._mode = "RGB"
self._size = (1, 1)
@@ -143,12 +146,12 @@ class TestImageFile:
assert im.format is None
assert im.get_format_mimetype() is None
- def test_oserror(self):
+ def test_oserror(self) -> None:
im = Image.new("RGB", (1, 1))
with pytest.raises(OSError):
im.save(BytesIO(), "JPEG2000", num_resolutions=2)
- def test_truncated(self):
+ def test_truncated(self) -> None:
b = BytesIO(
b"BM000000000000" # head_data
+ _binary.o32le(
@@ -163,7 +166,7 @@ class TestImageFile:
assert str(e.value) == "Truncated File Read"
@skip_unless_feature("zlib")
- def test_truncated_with_errors(self):
+ def test_truncated_with_errors(self) -> None:
with Image.open("Tests/images/truncated_image.png") as im:
with pytest.raises(OSError):
im.load()
@@ -173,7 +176,7 @@ class TestImageFile:
im.load()
@skip_unless_feature("zlib")
- def test_truncated_without_errors(self):
+ def test_truncated_without_errors(self) -> None:
with Image.open("Tests/images/truncated_image.png") as im:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
@@ -182,13 +185,13 @@ class TestImageFile:
ImageFile.LOAD_TRUNCATED_IMAGES = False
@skip_unless_feature("zlib")
- def test_broken_datastream_with_errors(self):
+ def test_broken_datastream_with_errors(self) -> None:
with Image.open("Tests/images/broken_data_stream.png") as im:
with pytest.raises(OSError):
im.load()
@skip_unless_feature("zlib")
- def test_broken_datastream_without_errors(self):
+ def test_broken_datastream_without_errors(self) -> None:
with Image.open("Tests/images/broken_data_stream.png") as im:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
@@ -207,7 +210,7 @@ class MockPyEncoder(ImageFile.PyEncoder):
def encode(self, buffer):
return 1, 1, b""
- def cleanup(self):
+ def cleanup(self) -> None:
self.cleanup_called = True
@@ -215,7 +218,7 @@ xoff, yoff, xsize, ysize = 10, 20, 100, 100
class MockImageFile(ImageFile.ImageFile):
- def _open(self):
+ def _open(self) -> None:
self.rawmode = "RGBA"
self._mode = "RGBA"
self._size = (200, 200)
@@ -224,7 +227,7 @@ class MockImageFile(ImageFile.ImageFile):
class CodecsTest:
@classmethod
- def setup_class(cls):
+ def setup_class(cls) -> None:
cls.decoder = MockPyDecoder(None)
cls.encoder = MockPyEncoder(None)
@@ -241,7 +244,7 @@ class CodecsTest:
class TestPyDecoder(CodecsTest):
- def test_setimage(self):
+ def test_setimage(self) -> None:
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
@@ -256,7 +259,7 @@ class TestPyDecoder(CodecsTest):
with pytest.raises(ValueError):
self.decoder.set_as_raw(b"\x00")
- def test_extents_none(self):
+ def test_extents_none(self) -> None:
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
@@ -269,7 +272,7 @@ class TestPyDecoder(CodecsTest):
assert self.decoder.state.xsize == 200
assert self.decoder.state.ysize == 200
- def test_negsize(self):
+ def test_negsize(self) -> None:
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
@@ -282,7 +285,7 @@ class TestPyDecoder(CodecsTest):
with pytest.raises(ValueError):
im.load()
- def test_oversize(self):
+ def test_oversize(self) -> None:
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
@@ -295,14 +298,14 @@ class TestPyDecoder(CodecsTest):
with pytest.raises(ValueError):
im.load()
- def test_decode(self):
+ def test_decode(self) -> None:
decoder = ImageFile.PyDecoder(None)
with pytest.raises(NotImplementedError):
decoder.decode(None)
class TestPyEncoder(CodecsTest):
- def test_setimage(self):
+ def test_setimage(self) -> None:
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
@@ -317,7 +320,7 @@ class TestPyEncoder(CodecsTest):
assert self.encoder.state.xsize == xsize
assert self.encoder.state.ysize == ysize
- def test_extents_none(self):
+ def test_extents_none(self) -> None:
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
@@ -331,7 +334,7 @@ class TestPyEncoder(CodecsTest):
assert self.encoder.state.xsize == 200
assert self.encoder.state.ysize == 200
- def test_negsize(self):
+ def test_negsize(self) -> None:
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
@@ -349,7 +352,7 @@ class TestPyEncoder(CodecsTest):
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")]
)
- def test_oversize(self):
+ def test_oversize(self) -> None:
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
@@ -369,7 +372,7 @@ class TestPyEncoder(CodecsTest):
[("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")],
)
- def test_encode(self):
+ def test_encode(self) -> None:
encoder = ImageFile.PyEncoder(None)
with pytest.raises(NotImplementedError):
encoder.encode(None)
@@ -385,6 +388,6 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None)
- def test_zero_height(self):
+ def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError):
Image.open("Tests/images/zero_height.j2k")
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index f2b7dedf0..909026dc8 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -1,9 +1,12 @@
+from __future__ import annotations
+
import copy
import os
import re
import shutil
import sys
from io import BytesIO
+from pathlib import Path
import pytest
from packaging.version import parse as parse_version
@@ -28,7 +31,7 @@ TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward"
pytestmark = skip_unless_feature("freetype2")
-def test_sanity():
+def test_sanity() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2"))
@@ -48,7 +51,7 @@ def font(layout_engine):
return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine)
-def test_font_properties(font):
+def test_font_properties(font) -> None:
assert font.path == FONT_PATH
assert font.size == FONT_SIZE
@@ -76,11 +79,12 @@ def _render(font, layout_engine):
return img
-def test_font_with_name(layout_engine):
- _render(FONT_PATH, layout_engine)
+@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
+def test_font_with_name(layout_engine, font) -> None:
+ _render(font, layout_engine)
-def test_font_with_filelike(layout_engine):
+def test_font_with_filelike(layout_engine) -> None:
def _font_as_bytes():
with open(FONT_PATH, "rb") as f:
font_bytes = BytesIO(f.read())
@@ -98,12 +102,12 @@ def test_font_with_filelike(layout_engine):
# _render(shared_bytes)
-def test_font_with_open_file(layout_engine):
+def test_font_with_open_file(layout_engine) -> None:
with open(FONT_PATH, "rb") as f:
_render(f, layout_engine)
-def test_render_equal(layout_engine):
+def test_render_equal(layout_engine) -> None:
img_path = _render(FONT_PATH, layout_engine)
with open(FONT_PATH, "rb") as f:
font_filelike = BytesIO(f.read())
@@ -112,7 +116,7 @@ def test_render_equal(layout_engine):
assert_image_equal(img_path, img_filelike)
-def test_non_ascii_path(tmp_path, layout_engine):
+def test_non_ascii_path(tmp_path: Path, layout_engine) -> None:
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
try:
shutil.copy(FONT_PATH, tempfile)
@@ -122,7 +126,7 @@ def test_non_ascii_path(tmp_path, layout_engine):
ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine)
-def test_transparent_background(font):
+def test_transparent_background(font) -> None:
im = Image.new(mode="RGBA", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -136,7 +140,7 @@ def test_transparent_background(font):
assert_image_similar_tofile(im.convert("L"), target, 0.01)
-def test_I16(font):
+def test_I16(font) -> None:
im = Image.new(mode="I;16", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -149,7 +153,7 @@ def test_I16(font):
assert_image_similar_tofile(im.convert("L"), target, 0.01)
-def test_textbbox_equal(font):
+def test_textbbox_equal(font) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -178,7 +182,7 @@ def test_textbbox_equal(font):
)
def test_getlength(
text, mode, fontname, size, layout_engine, length_basic, length_raqm
-):
+) -> None:
f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
im = Image.new(mode, (1, 1), 0)
@@ -193,7 +197,7 @@ def test_getlength(
assert length == length_raqm
-def test_float_size():
+def test_float_size() -> None:
lengths = []
for size in (48, 48.5, 49):
f = ImageFont.truetype(
@@ -203,7 +207,7 @@ def test_float_size():
assert lengths[0] != lengths[1] != lengths[2]
-def test_render_multiline(font):
+def test_render_multiline(font) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
line_spacing = font.getbbox("A")[3] + 4
@@ -219,7 +223,7 @@ def test_render_multiline(font):
assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
-def test_render_multiline_text(font):
+def test_render_multiline_text(font) -> None:
# Test that text() correctly connects to multiline_text()
# and that align defaults to left
im = Image.new(mode="RGB", size=(300, 100))
@@ -239,7 +243,7 @@ def test_render_multiline_text(font):
@pytest.mark.parametrize(
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
)
-def test_render_multiline_text_align(font, align, ext):
+def test_render_multiline_text_align(font, align, ext) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align)
@@ -247,7 +251,7 @@ def test_render_multiline_text_align(font, align, ext):
assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01)
-def test_unknown_align(font):
+def test_unknown_align(font) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -256,14 +260,14 @@ def test_unknown_align(font):
draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown")
-def test_draw_align(font):
+def test_draw_align(font) -> None:
im = Image.new("RGB", (300, 100), "white")
draw = ImageDraw.Draw(im)
line = "some text"
draw.text((100, 40), line, (0, 0, 0), font=font, align="left")
-def test_multiline_bbox(font):
+def test_multiline_bbox(font) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -281,7 +285,7 @@ def test_multiline_bbox(font):
draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4)
-def test_multiline_width(font):
+def test_multiline_width(font) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -291,7 +295,7 @@ def test_multiline_width(font):
)
-def test_multiline_spacing(font):
+def test_multiline_spacing(font) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10)
@@ -302,9 +306,9 @@ def test_multiline_spacing(font):
@pytest.mark.parametrize(
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
)
-def test_rotated_transposed_font(font, orientation):
- img_grey = Image.new("L", (100, 100))
- draw = ImageDraw.Draw(img_grey)
+def test_rotated_transposed_font(font, orientation) -> None:
+ img_gray = Image.new("L", (100, 100))
+ draw = ImageDraw.Draw(img_gray)
word = "testing"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -343,9 +347,9 @@ def test_rotated_transposed_font(font, orientation):
Image.Transpose.FLIP_TOP_BOTTOM,
),
)
-def test_unrotated_transposed_font(font, orientation):
- img_grey = Image.new("L", (100, 100))
- draw = ImageDraw.Draw(img_grey)
+def test_unrotated_transposed_font(font, orientation) -> None:
+ img_gray = Image.new("L", (100, 100))
+ draw = ImageDraw.Draw(img_gray)
word = "testing"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -378,7 +382,7 @@ def test_unrotated_transposed_font(font, orientation):
@pytest.mark.parametrize(
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
)
-def test_rotated_transposed_font_get_mask(font, orientation):
+def test_rotated_transposed_font_get_mask(font, orientation) -> None:
# Arrange
text = "mask this"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -399,7 +403,7 @@ def test_rotated_transposed_font_get_mask(font, orientation):
Image.Transpose.FLIP_TOP_BOTTOM,
),
)
-def test_unrotated_transposed_font_get_mask(font, orientation):
+def test_unrotated_transposed_font_get_mask(font, orientation) -> None:
# Arrange
text = "mask this"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -411,11 +415,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation):
assert mask.size == (108, 13)
-def test_free_type_font_get_name(font):
+def test_free_type_font_get_name(font) -> None:
assert ("FreeMono", "Regular") == font.getname()
-def test_free_type_font_get_metrics(font):
+def test_free_type_font_get_metrics(font) -> None:
ascent, descent = font.getmetrics()
assert isinstance(ascent, int)
@@ -423,7 +427,7 @@ def test_free_type_font_get_metrics(font):
assert (ascent, descent) == (16, 4)
-def test_free_type_font_get_mask(font):
+def test_free_type_font_get_mask(font) -> None:
# Arrange
text = "mask this"
@@ -434,7 +438,7 @@ def test_free_type_font_get_mask(font):
assert mask.size == (108, 13)
-def test_load_path_not_found():
+def test_load_path_not_found() -> None:
# Arrange
filename = "somefilenamethatdoesntexist.ttf"
@@ -445,13 +449,13 @@ def test_load_path_not_found():
ImageFont.truetype(filename)
-def test_load_non_font_bytes():
+def test_load_non_font_bytes() -> None:
with open("Tests/images/hopper.jpg", "rb") as f:
with pytest.raises(OSError):
ImageFont.truetype(f)
-def test_default_font():
+def test_default_font() -> None:
# Arrange
txt = "This is a default font using FreeType support."
im = Image.new(mode="RGB", size=(300, 100))
@@ -469,16 +473,16 @@ def test_default_font():
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
-def test_getbbox(font, mode):
+def test_getbbox(font, mode) -> None:
assert (0, 4, 12, 16) == font.getbbox("A", mode)
-def test_getbbox_empty(font):
+def test_getbbox_empty(font) -> None:
# issue #2614, should not crash.
assert (0, 0, 0, 0) == font.getbbox("")
-def test_render_empty(font):
+def test_render_empty(font) -> None:
# issue 2666
im = Image.new(mode="RGB", size=(300, 100))
target = im.copy()
@@ -488,7 +492,7 @@ def test_render_empty(font):
assert_image_equal(im, target)
-def test_unicode_extended(layout_engine):
+def test_unicode_extended(layout_engine) -> None:
# issue #3777
text = "A\u278A\U0001F12B"
target = "Tests/images/unicode_extended.png"
@@ -511,8 +515,8 @@ def test_unicode_extended(layout_engine):
(("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
)
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
-def test_find_font(monkeypatch, platform, font_directory):
- def _test_fake_loading_font(path_to_fake, fontname):
+def test_find_font(monkeypatch, platform, font_directory) -> None:
+ def _test_fake_loading_font(path_to_fake, fontname) -> None:
# Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
with monkeypatch.context() as m:
@@ -563,7 +567,7 @@ def test_find_font(monkeypatch, platform, font_directory):
_test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate")
-def test_imagefont_getters(font):
+def test_imagefont_getters(font) -> None:
assert font.getmetrics() == (16, 4)
assert font.font.ascent == 16
assert font.font.descent == 4
@@ -584,7 +588,7 @@ def test_imagefont_getters(font):
@pytest.mark.parametrize("stroke_width", (0, 2))
-def test_getsize_stroke(font, stroke_width):
+def test_getsize_stroke(font, stroke_width) -> None:
assert font.getbbox("A", stroke_width=stroke_width) == (
0 - stroke_width,
4 - stroke_width,
@@ -593,7 +597,7 @@ def test_getsize_stroke(font, stroke_width):
)
-def test_complex_font_settings():
+def test_complex_font_settings() -> None:
t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC)
with pytest.raises(KeyError):
t.getmask("абвг", direction="rtl")
@@ -603,7 +607,7 @@ def test_complex_font_settings():
t.getmask("абвг", language="sr")
-def test_variation_get(font):
+def test_variation_get(font) -> None:
freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
@@ -673,7 +677,7 @@ def _check_text(font, path, epsilon):
raise
-def test_variation_set_by_name(font):
+def test_variation_set_by_name(font) -> None:
freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
@@ -698,7 +702,7 @@ def test_variation_set_by_name(font):
_check_text(font, "Tests/images/variation_tiny_name.png", 40)
-def test_variation_set_by_axes(font):
+def test_variation_set_by_axes(font) -> None:
freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
@@ -733,7 +737,7 @@ def test_variation_set_by_axes(font):
),
ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
)
-def test_anchor(layout_engine, anchor, left, top):
+def test_anchor(layout_engine, anchor, left, top) -> None:
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
@@ -778,7 +782,7 @@ def test_anchor(layout_engine, anchor, left, top):
("md", "center"),
),
)
-def test_anchor_multiline(layout_engine, anchor, align):
+def test_anchor_multiline(layout_engine, anchor, align) -> None:
target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
text = "a\nlong\ntext sample"
@@ -796,7 +800,7 @@ def test_anchor_multiline(layout_engine, anchor, align):
assert_image_similar_tofile(im, target, 4)
-def test_anchor_invalid(font):
+def test_anchor_invalid(font) -> None:
im = Image.new("RGB", (100, 100), "white")
d = ImageDraw.Draw(im)
d.font = font
@@ -822,7 +826,7 @@ def test_anchor_invalid(font):
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
-def test_bitmap_font(layout_engine, bpp):
+def test_bitmap_font(layout_engine, bpp) -> None:
text = "Bitmap Font"
layout_name = ["basic", "raqm"][layout_engine]
target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
@@ -839,7 +843,7 @@ def test_bitmap_font(layout_engine, bpp):
assert_image_equal_tofile(im, target)
-def test_bitmap_font_stroke(layout_engine):
+def test_bitmap_font_stroke(layout_engine) -> None:
text = "Bitmap Font"
layout_name = ["basic", "raqm"][layout_engine]
target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
@@ -856,7 +860,20 @@ def test_bitmap_font_stroke(layout_engine):
assert_image_similar_tofile(im, target, 0.03)
-def test_standard_embedded_color(layout_engine):
+@pytest.mark.parametrize("embedded_color", (False, True))
+def test_bitmap_blend(layout_engine, embedded_color) -> None:
+ font = ImageFont.truetype(
+ "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine
+ )
+
+ im = Image.new("RGBA", (128, 96), "white")
+ d = ImageDraw.Draw(im)
+ d.text((16, 16), "AA", font=font, fill="#8E2F52", embedded_color=embedded_color)
+
+ assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png")
+
+
+def test_standard_embedded_color(layout_engine) -> None:
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
ttf.getbbox(txt)
@@ -891,43 +908,41 @@ def test_float_coord(layout_engine, fontmode):
raise
-def test_cbdt(layout_engine):
+def test_cbdt(layout_engine) -> None:
try:
font = ImageFont.truetype(
- "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine
+ "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
)
- im = Image.new("RGB", (150, 150), "white")
+ im = Image.new("RGB", (128, 96), "white")
d = ImageDraw.Draw(im)
- d.text((10, 10), "\U0001f469", font=font, embedded_color=True)
+ d.text((16, 16), "AB", font=font, embedded_color=True)
- assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2)
+ assert_image_equal_tofile(im, "Tests/images/cbdt.png")
except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or CBDT support")
-def test_cbdt_mask(layout_engine):
+def test_cbdt_mask(layout_engine) -> None:
try:
font = ImageFont.truetype(
- "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine
+ "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
)
- im = Image.new("RGB", (150, 150), "white")
+ im = Image.new("RGB", (128, 96), "white")
d = ImageDraw.Draw(im)
- d.text((10, 10), "\U0001f469", "black", font=font)
+ d.text((16, 16), "AB", "green", font=font)
- assert_image_similar_tofile(
- im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2
- )
+ assert_image_equal_tofile(im, "Tests/images/cbdt_mask.png")
except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or CBDT support")
-def test_sbix(layout_engine):
+def test_sbix(layout_engine) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
@@ -944,7 +959,7 @@ def test_sbix(layout_engine):
pytest.skip("freetype compiled without libpng or SBIX support")
-def test_sbix_mask(layout_engine):
+def test_sbix_mask(layout_engine) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
@@ -962,7 +977,7 @@ def test_sbix_mask(layout_engine):
@skip_unless_feature_version("freetype2", "2.10.0")
-def test_colr(layout_engine):
+def test_colr(layout_engine) -> None:
font = ImageFont.truetype(
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
size=64,
@@ -978,7 +993,7 @@ def test_colr(layout_engine):
@skip_unless_feature_version("freetype2", "2.10.0")
-def test_colr_mask(layout_engine):
+def test_colr_mask(layout_engine) -> None:
font = ImageFont.truetype(
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
size=64,
@@ -993,7 +1008,7 @@ def test_colr_mask(layout_engine):
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
-def test_woff2(layout_engine):
+def test_woff2(layout_engine) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/OpenSans.woff2",
@@ -1012,7 +1027,7 @@ def test_woff2(layout_engine):
assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5)
-def test_render_mono_size():
+def test_render_mono_size() -> None:
# issue 4177
im = Image.new("P", (100, 30), "white")
@@ -1027,7 +1042,7 @@ def test_render_mono_size():
assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
-def test_too_many_characters(font):
+def test_too_many_characters(font) -> None:
with pytest.raises(ValueError):
font.getlength("A" * 1_000_001)
with pytest.raises(ValueError):
@@ -1039,11 +1054,13 @@ def test_too_many_characters(font):
with pytest.raises(ValueError):
transposed_font.getlength("A" * 1_000_001)
- default_font = ImageFont.load_default()
+ imagefont = ImageFont.ImageFont()
with pytest.raises(ValueError):
- default_font.getlength("A" * 1_000_001)
+ imagefont.getlength("A" * 1_000_001)
with pytest.raises(ValueError):
- default_font.getbbox("A" * 1_000_001)
+ imagefont.getbbox("A" * 1_000_001)
+ with pytest.raises(ValueError):
+ imagefont.getmask("A" * 1_000_001)
@pytest.mark.parametrize(
@@ -1053,14 +1070,14 @@ def test_too_many_characters(font):
"Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
],
)
-def test_oom(test_file):
+def test_oom(test_file) -> None:
with open(test_file, "rb") as f:
font = ImageFont.truetype(BytesIO(f.read()))
with pytest.raises(Image.DecompressionBombError):
font.getmask("Test Text")
-def test_raqm_missing_warning(monkeypatch):
+def test_raqm_missing_warning(monkeypatch) -> None:
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
with pytest.warns(UserWarning) as record:
font = ImageFont.truetype(
@@ -1071,3 +1088,9 @@ def test_raqm_missing_warning(monkeypatch):
"Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout."
)
+
+
+@pytest.mark.parametrize("size", [-1, 0])
+def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None:
+ with pytest.raises(ValueError):
+ ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py
index 6099b04e4..325e7ef21 100644
--- a/Tests/test_imagefontctl.py
+++ b/Tests/test_imagefontctl.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageDraw, ImageFont
@@ -10,7 +12,7 @@ FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
pytestmark = skip_unless_feature("raqm")
-def test_english():
+def test_english() -> None:
# smoke test, this should not fail
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -18,7 +20,7 @@ def test_english():
draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr")
-def test_complex_text():
+def test_complex_text() -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -29,7 +31,7 @@ def test_complex_text():
assert_image_similar_tofile(im, target, 0.5)
-def test_y_offset():
+def test_y_offset() -> None:
ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -40,7 +42,7 @@ def test_y_offset():
assert_image_similar_tofile(im, target, 1.7)
-def test_complex_unicode_text():
+def test_complex_unicode_text() -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -60,7 +62,7 @@ def test_complex_unicode_text():
assert_image_similar_tofile(im, target, 2.33)
-def test_text_direction_rtl():
+def test_text_direction_rtl() -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -71,7 +73,7 @@ def test_text_direction_rtl():
assert_image_similar_tofile(im, target, 0.5)
-def test_text_direction_ltr():
+def test_text_direction_ltr() -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -82,7 +84,7 @@ def test_text_direction_ltr():
assert_image_similar_tofile(im, target, 0.5)
-def test_text_direction_rtl2():
+def test_text_direction_rtl2() -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -93,7 +95,7 @@ def test_text_direction_rtl2():
assert_image_similar_tofile(im, target, 0.5)
-def test_text_direction_ttb():
+def test_text_direction_ttb() -> None:
ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE)
im = Image.new(mode="RGB", size=(100, 300))
@@ -108,7 +110,7 @@ def test_text_direction_ttb():
assert_image_similar_tofile(im, target, 2.8)
-def test_text_direction_ttb_stroke():
+def test_text_direction_ttb_stroke() -> None:
ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50)
im = Image.new(mode="RGB", size=(100, 300))
@@ -131,7 +133,7 @@ def test_text_direction_ttb_stroke():
assert_image_similar_tofile(im, target, 19.4)
-def test_ligature_features():
+def test_ligature_features() -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -144,7 +146,7 @@ def test_ligature_features():
assert liga_bbox == (0, 4, 13, 19)
-def test_kerning_features():
+def test_kerning_features() -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -155,7 +157,7 @@ def test_kerning_features():
assert_image_similar_tofile(im, target, 0.5)
-def test_arabictext_features():
+def test_arabictext_features() -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -172,7 +174,7 @@ def test_arabictext_features():
assert_image_similar_tofile(im, target, 0.5)
-def test_x_max_and_y_offset():
+def test_x_max_and_y_offset() -> None:
ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40)
im = Image.new(mode="RGB", size=(50, 100))
@@ -183,7 +185,7 @@ def test_x_max_and_y_offset():
assert_image_similar_tofile(im, target, 0.5)
-def test_language():
+def test_language() -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode="RGB", size=(300, 100))
@@ -206,7 +208,7 @@ def test_language():
),
ids=("None", "ltr", "rtl2", "rtl", "ttb"),
)
-def test_getlength(mode, text, direction, expected):
+def test_getlength(mode, text, direction, expected) -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im)
@@ -228,7 +230,7 @@ def test_getlength(mode, text, direction, expected):
("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"),
ids=("caron-above", "caron-below", "double-breve", "overline"),
)
-def test_getlength_combine(mode, direction, text):
+def test_getlength_combine(mode, direction, text) -> None:
if text == "i\u0305i" and direction == "ttb":
pytest.skip("fails with this font")
@@ -248,7 +250,7 @@ def test_getlength_combine(mode, direction, text):
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
-def test_anchor_ttb(anchor):
+def test_anchor_ttb(anchor) -> None:
text = "f"
path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120)
@@ -304,7 +306,7 @@ combine_tests = (
@pytest.mark.parametrize(
"name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests]
)
-def test_combine(name, text, dir, anchor, epsilon):
+def test_combine(name, text, dir, anchor, epsilon) -> None:
path = f"Tests/images/test_combine_{name}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
@@ -335,7 +337,7 @@ def test_combine(name, text, dir, anchor, epsilon):
("rm", "right"), # pass with getsize
),
)
-def test_combine_multiline(anchor, align):
+def test_combine_multiline(anchor, align) -> None:
# test that multiline text uses getlength, not getsize or getbbox
path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png"
@@ -353,7 +355,7 @@ def test_combine_multiline(anchor, align):
assert_image_similar_tofile(im, path, 0.015)
-def test_anchor_invalid_ttb():
+def test_anchor_invalid_ttb() -> None:
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new("RGB", (100, 100), "white")
d = ImageDraw.Draw(im)
diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py
index c30463e81..3b1c14b4e 100644
--- a/Tests/test_imagefontpil.py
+++ b/Tests/test_imagefontpil.py
@@ -1,16 +1,27 @@
+from __future__ import annotations
+
+import struct
+from io import BytesIO
+
import pytest
-from PIL import Image, ImageDraw, ImageFont, features
+from PIL import Image, ImageDraw, ImageFont, _util, features
from .helper import assert_image_equal_tofile
-pytestmark = pytest.mark.skipif(
- features.check_module("freetype2"),
- reason="PILfont superseded if FreeType is supported",
-)
+original_core = ImageFont.core
-def test_default_font():
+def setup_module() -> None:
+ if features.check_module("freetype2"):
+ ImageFont.core = _util.DeferredError(ImportError)
+
+
+def teardown_module() -> None:
+ ImageFont.core = original_core
+
+
+def test_default_font() -> None:
# Arrange
txt = 'This is a "better than nothing" default font.'
im = Image.new(mode="RGB", size=(300, 100))
@@ -24,12 +35,12 @@ def test_default_font():
assert_image_equal_tofile(im, "Tests/images/default_font.png")
-def test_size_without_freetype():
+def test_size_without_freetype() -> None:
with pytest.raises(ImportError):
ImageFont.load_default(size=14)
-def test_unicode():
+def test_unicode() -> None:
# should not segfault, should return UnicodeDecodeError
# issue #2826
font = ImageFont.load_default()
@@ -37,9 +48,31 @@ def test_unicode():
font.getbbox("’")
-def test_textbbox():
+def test_textbbox() -> None:
im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im)
default_font = ImageFont.load_default()
assert d.textlength("test", font=default_font) == 24
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
+
+
+def test_decompression_bomb() -> None:
+ glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256)
+ fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
+
+ font = ImageFont.ImageFont()
+ font._load_pilfont_data(fp, Image.new("L", (256, 256)))
+ with pytest.raises(Image.DecompressionBombError):
+ font.getmask("A" * 1_000_000)
+
+
+@pytest.mark.timeout(4)
+def test_oom() -> None:
+ glyph = struct.pack(
+ ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767
+ )
+ fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
+
+ font = ImageFont.ImageFont()
+ font._load_pilfont_data(fp, Image.new("L", (1, 1)))
+ font.getmask("A" * 1_000_000)
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index f8059eca4..40c1d323e 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import os
import shutil
import subprocess
@@ -11,10 +13,14 @@ from .helper import assert_image_equal_tofile, skip_unless_feature
class TestImageGrab:
+ @pytest.mark.skipif(
+ os.environ.get("USERNAME") == "ContainerAdministrator",
+ reason="can't grab screen when running in Docker",
+ )
@pytest.mark.skipif(
sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
)
- def test_grab(self):
+ def test_grab(self) -> None:
ImageGrab.grab()
ImageGrab.grab(include_layered_windows=True)
ImageGrab.grab(all_screens=True)
@@ -23,7 +29,7 @@ class TestImageGrab:
assert im.size == (40, 60)
@skip_unless_feature("xcb")
- def test_grab_x11(self):
+ def test_grab_x11(self) -> None:
try:
if sys.platform not in ("win32", "darwin"):
ImageGrab.grab()
@@ -33,7 +39,7 @@ class TestImageGrab:
pytest.skip(str(e))
@pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB")
- def test_grab_no_xcb(self):
+ def test_grab_no_xcb(self) -> None:
if sys.platform not in ("win32", "darwin") and not shutil.which(
"gnome-screenshot"
):
@@ -46,12 +52,12 @@ class TestImageGrab:
assert str(e.value).startswith("Pillow was built without XCB support")
@skip_unless_feature("xcb")
- def test_grab_invalid_xdisplay(self):
+ def test_grab_invalid_xdisplay(self) -> None:
with pytest.raises(OSError) as e:
ImageGrab.grab(xdisplay="error.test:0.0")
assert str(e.value).startswith("X connection failed")
- def test_grabclipboard(self):
+ def test_grabclipboard(self) -> None:
if sys.platform == "darwin":
subprocess.call(["screencapture", "-cx"])
elif sys.platform == "win32":
@@ -76,7 +82,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
ImageGrab.grabclipboard()
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
- def test_grabclipboard_file(self):
+ def test_grabclipboard_file(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"')
p.communicate()
@@ -86,7 +92,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
- def test_grabclipboard_png(self):
+ def test_grabclipboard_png(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
p.stdin.write(
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
@@ -107,7 +113,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
reason="Linux with wl-clipboard only",
)
@pytest.mark.parametrize("ext", ("gif", "png", "ico"))
- def test_grabclipboard_wl_clipboard(self, ext):
+ def test_grabclipboard_wl_clipboard(self, ext) -> None:
image_path = "Tests/images/hopper." + ext
with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp)
diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py
index fe7ac9a7a..ea6e80f1e 100644
--- a/Tests/test_imagemath.py
+++ b/Tests/test_imagemath.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageMath
@@ -22,7 +24,7 @@ B2 = B.resize((2, 2))
images = {"A": A, "B": B, "F": F, "I": I}
-def test_sanity():
+def test_sanity() -> None:
assert ImageMath.eval("1") == 1
assert ImageMath.eval("1+A", A=2) == 3
assert pixel(ImageMath.eval("A+B", A=A, B=B)) == "I 3"
@@ -31,7 +33,7 @@ def test_sanity():
assert pixel(ImageMath.eval("int(float(A)+B)", images)) == "I 3"
-def test_ops():
+def test_ops() -> None:
assert pixel(ImageMath.eval("-A", images)) == "I -1"
assert pixel(ImageMath.eval("+B", images)) == "L 2"
@@ -58,41 +60,51 @@ def test_ops():
"(lambda: (lambda: exec('pass'))())()",
),
)
-def test_prevent_exec(expression):
+def test_prevent_exec(expression) -> None:
with pytest.raises(ValueError):
ImageMath.eval(expression)
-def test_logical():
+def test_prevent_double_underscores() -> None:
+ with pytest.raises(ValueError):
+ ImageMath.eval("1", {"__": None})
+
+
+def test_prevent_builtins() -> None:
+ with pytest.raises(ValueError):
+ ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None})
+
+
+def test_logical() -> None:
assert pixel(ImageMath.eval("not A", images)) == 0
assert pixel(ImageMath.eval("A and B", images)) == "L 2"
assert pixel(ImageMath.eval("A or B", images)) == "L 1"
-def test_convert():
+def test_convert() -> None:
assert pixel(ImageMath.eval("convert(A+B, 'L')", images)) == "L 3"
assert pixel(ImageMath.eval("convert(A+B, '1')", images)) == "1 0"
assert pixel(ImageMath.eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)"
-def test_compare():
+def test_compare() -> None:
assert pixel(ImageMath.eval("min(A, B)", images)) == "I 1"
assert pixel(ImageMath.eval("max(A, B)", images)) == "I 2"
assert pixel(ImageMath.eval("A == 1", images)) == "I 1"
assert pixel(ImageMath.eval("A == 2", images)) == "I 0"
-def test_one_image_larger():
+def test_one_image_larger() -> None:
assert pixel(ImageMath.eval("A+B", A=A2, B=B)) == "I 3"
assert pixel(ImageMath.eval("A+B", A=A, B=B2)) == "I 3"
-def test_abs():
+def test_abs() -> None:
assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1"
assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2"
-def test_binary_mod():
+def test_binary_mod() -> None:
assert pixel(ImageMath.eval("A%A", A=A)) == "I 0"
assert pixel(ImageMath.eval("B%B", B=B)) == "I 0"
assert pixel(ImageMath.eval("A%B", A=A, B=B)) == "I 1"
@@ -101,90 +113,90 @@ def test_binary_mod():
assert pixel(ImageMath.eval("Z%B", B=B, Z=Z)) == "I 0"
-def test_bitwise_invert():
+def test_bitwise_invert() -> None:
assert pixel(ImageMath.eval("~Z", Z=Z)) == "I -1"
assert pixel(ImageMath.eval("~A", A=A)) == "I -2"
assert pixel(ImageMath.eval("~B", B=B)) == "I -3"
-def test_bitwise_and():
+def test_bitwise_and() -> None:
assert pixel(ImageMath.eval("Z&Z", A=A, Z=Z)) == "I 0"
assert pixel(ImageMath.eval("Z&A", A=A, Z=Z)) == "I 0"
assert pixel(ImageMath.eval("A&Z", A=A, Z=Z)) == "I 0"
assert pixel(ImageMath.eval("A&A", A=A, Z=Z)) == "I 1"
-def test_bitwise_or():
+def test_bitwise_or() -> None:
assert pixel(ImageMath.eval("Z|Z", A=A, Z=Z)) == "I 0"
assert pixel(ImageMath.eval("Z|A", A=A, Z=Z)) == "I 1"
assert pixel(ImageMath.eval("A|Z", A=A, Z=Z)) == "I 1"
assert pixel(ImageMath.eval("A|A", A=A, Z=Z)) == "I 1"
-def test_bitwise_xor():
+def test_bitwise_xor() -> None:
assert pixel(ImageMath.eval("Z^Z", A=A, Z=Z)) == "I 0"
assert pixel(ImageMath.eval("Z^A", A=A, Z=Z)) == "I 1"
assert pixel(ImageMath.eval("A^Z", A=A, Z=Z)) == "I 1"
assert pixel(ImageMath.eval("A^A", A=A, Z=Z)) == "I 0"
-def test_bitwise_leftshift():
+def test_bitwise_leftshift() -> None:
assert pixel(ImageMath.eval("Z<<0", Z=Z)) == "I 0"
assert pixel(ImageMath.eval("Z<<1", Z=Z)) == "I 0"
assert pixel(ImageMath.eval("A<<0", A=A)) == "I 1"
assert pixel(ImageMath.eval("A<<1", A=A)) == "I 2"
-def test_bitwise_rightshift():
+def test_bitwise_rightshift() -> None:
assert pixel(ImageMath.eval("Z>>0", Z=Z)) == "I 0"
assert pixel(ImageMath.eval("Z>>1", Z=Z)) == "I 0"
assert pixel(ImageMath.eval("A>>0", A=A)) == "I 1"
assert pixel(ImageMath.eval("A>>1", A=A)) == "I 0"
-def test_logical_eq():
+def test_logical_eq() -> None:
assert pixel(ImageMath.eval("A==A", A=A)) == "I 1"
assert pixel(ImageMath.eval("B==B", B=B)) == "I 1"
assert pixel(ImageMath.eval("A==B", A=A, B=B)) == "I 0"
assert pixel(ImageMath.eval("B==A", A=A, B=B)) == "I 0"
-def test_logical_ne():
+def test_logical_ne() -> None:
assert pixel(ImageMath.eval("A!=A", A=A)) == "I 0"
assert pixel(ImageMath.eval("B!=B", B=B)) == "I 0"
assert pixel(ImageMath.eval("A!=B", A=A, B=B)) == "I 1"
assert pixel(ImageMath.eval("B!=A", A=A, B=B)) == "I 1"
-def test_logical_lt():
+def test_logical_lt() -> None:
assert pixel(ImageMath.eval("A None:
assert pixel(ImageMath.eval("A<=A", A=A)) == "I 1"
assert pixel(ImageMath.eval("B<=B", B=B)) == "I 1"
assert pixel(ImageMath.eval("A<=B", A=A, B=B)) == "I 1"
assert pixel(ImageMath.eval("B<=A", A=A, B=B)) == "I 0"
-def test_logical_gt():
+def test_logical_gt() -> None:
assert pixel(ImageMath.eval("A>A", A=A)) == "I 0"
assert pixel(ImageMath.eval("B>B", B=B)) == "I 0"
assert pixel(ImageMath.eval("A>B", A=A, B=B)) == "I 0"
assert pixel(ImageMath.eval("B>A", A=A, B=B)) == "I 1"
-def test_logical_ge():
+def test_logical_ge() -> None:
assert pixel(ImageMath.eval("A>=A", A=A)) == "I 1"
assert pixel(ImageMath.eval("B>=B", B=B)) == "I 1"
assert pixel(ImageMath.eval("A>=B", A=A, B=B)) == "I 0"
assert pixel(ImageMath.eval("B>=A", A=A, B=B)) == "I 1"
-def test_logical_equal():
+def test_logical_equal() -> None:
assert pixel(ImageMath.eval("equal(A, A)", A=A)) == "I 1"
assert pixel(ImageMath.eval("equal(B, B)", B=B)) == "I 1"
assert pixel(ImageMath.eval("equal(Z, Z)", Z=Z)) == "I 1"
@@ -193,7 +205,7 @@ def test_logical_equal():
assert pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)) == "I 0"
-def test_logical_not_equal():
+def test_logical_not_equal() -> None:
assert pixel(ImageMath.eval("notequal(A, A)", A=A)) == "I 0"
assert pixel(ImageMath.eval("notequal(B, B)", B=B)) == "I 0"
assert pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)) == "I 0"
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index 29c71f917..0b0c6d2d3 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -1,4 +1,8 @@
# Test the ImageMorphology functionality
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImageMorph, _imagingmorph
@@ -48,27 +52,18 @@ def img_string_normalize(im):
return img_to_string(string_to_img(im))
-def assert_img_equal_img_string(a, b_string):
+def assert_img_equal_img_string(a, b_string) -> None:
assert img_to_string(a) == img_string_normalize(b_string)
-def test_str_to_img():
+def test_str_to_img() -> None:
assert_image_equal_tofile(A, "Tests/images/morph_a.png")
-def create_lut():
- for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"):
- lb = ImageMorph.LutBuilder(op_name=op)
- lut = lb.build_lut()
- with open(f"Tests/images/{op}.lut", "wb") as f:
- f.write(lut)
-
-
-# create_lut()
@pytest.mark.parametrize(
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
)
-def test_lut(op):
+def test_lut(op) -> None:
lb = ImageMorph.LutBuilder(op_name=op)
assert lb.get_lut() is None
@@ -77,7 +72,7 @@ def test_lut(op):
assert lut == bytearray(f.read())
-def test_no_operator_loaded():
+def test_no_operator_loaded() -> None:
mop = ImageMorph.MorphOp()
with pytest.raises(Exception) as e:
mop.apply(None)
@@ -91,7 +86,7 @@ def test_no_operator_loaded():
# Test the named patterns
-def test_erosion8():
+def test_erosion8() -> None:
# erosion8
mop = ImageMorph.MorphOp(op_name="erosion8")
count, Aout = mop.apply(A)
@@ -110,7 +105,7 @@ def test_erosion8():
)
-def test_dialation8():
+def test_dialation8() -> None:
# dialation8
mop = ImageMorph.MorphOp(op_name="dilation8")
count, Aout = mop.apply(A)
@@ -129,7 +124,7 @@ def test_dialation8():
)
-def test_erosion4():
+def test_erosion4() -> None:
# erosion4
mop = ImageMorph.MorphOp(op_name="dilation4")
count, Aout = mop.apply(A)
@@ -148,7 +143,7 @@ def test_erosion4():
)
-def test_edge():
+def test_edge() -> None:
# edge
mop = ImageMorph.MorphOp(op_name="edge")
count, Aout = mop.apply(A)
@@ -167,7 +162,7 @@ def test_edge():
)
-def test_corner():
+def test_corner() -> None:
# Create a corner detector pattern
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"])
count, Aout = mop.apply(A)
@@ -195,7 +190,7 @@ def test_corner():
assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4))
-def test_mirroring():
+def test_mirroring() -> None:
# Test 'M' for mirroring
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"])
count, Aout = mop.apply(A)
@@ -214,7 +209,7 @@ def test_mirroring():
)
-def test_negate():
+def test_negate() -> None:
# Test 'N' for negate
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"])
count, Aout = mop.apply(A)
@@ -233,7 +228,7 @@ def test_negate():
)
-def test_incorrect_mode():
+def test_incorrect_mode() -> None:
im = hopper("RGB")
mop = ImageMorph.MorphOp(op_name="erosion8")
@@ -248,7 +243,7 @@ def test_incorrect_mode():
assert str(e.value) == "Image mode must be L"
-def test_add_patterns():
+def test_add_patterns() -> None:
# Arrange
lb = ImageMorph.LutBuilder(op_name="corner")
assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"]
@@ -266,12 +261,12 @@ def test_add_patterns():
]
-def test_unknown_pattern():
+def test_unknown_pattern() -> None:
with pytest.raises(Exception):
ImageMorph.LutBuilder(op_name="unknown")
-def test_pattern_syntax_error():
+def test_pattern_syntax_error() -> None:
# Arrange
lb = ImageMorph.LutBuilder(op_name="corner")
new_patterns = ["a pattern with a syntax error"]
@@ -283,7 +278,7 @@ def test_pattern_syntax_error():
assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"'
-def test_load_invalid_mrl():
+def test_load_invalid_mrl() -> None:
# Arrange
invalid_mrl = "Tests/images/hopper.png"
mop = ImageMorph.MorphOp()
@@ -294,7 +289,7 @@ def test_load_invalid_mrl():
assert str(e.value) == "Wrong size operator file!"
-def test_roundtrip_mrl(tmp_path):
+def test_roundtrip_mrl(tmp_path: Path) -> None:
# Arrange
tempfile = str(tmp_path / "temp.mrl")
mop = ImageMorph.MorphOp(op_name="corner")
@@ -308,7 +303,7 @@ def test_roundtrip_mrl(tmp_path):
assert mop.lut == initial_lut
-def test_set_lut():
+def test_set_lut() -> None:
# Arrange
lb = ImageMorph.LutBuilder(op_name="corner")
lut = lb.build_lut()
@@ -321,7 +316,7 @@ def test_set_lut():
assert mop.lut == lut
-def test_wrong_mode():
+def test_wrong_mode() -> None:
lut = ImageMorph.LutBuilder(op_name="corner").build_lut()
imrgb = Image.new("RGB", (10, 10))
iml = Image.new("L", (10, 10))
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index 6d153ccea..50bf404ae 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageDraw, ImageOps, ImageStat, features
@@ -20,7 +22,7 @@ class Deformer:
deformer = Deformer()
-def test_sanity():
+def test_sanity() -> None:
ImageOps.autocontrast(hopper("L"))
ImageOps.autocontrast(hopper("RGB"))
@@ -82,7 +84,7 @@ def test_sanity():
ImageOps.exif_transpose(hopper("RGB"))
-def test_1pxfit():
+def test_1pxfit() -> None:
# Division by zero in equalize if image is 1 pixel high
newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35))
assert newimg.size == (35, 35)
@@ -94,7 +96,7 @@ def test_1pxfit():
assert newimg.size == (35, 35)
-def test_fit_same_ratio():
+def test_fit_same_ratio() -> None:
# The ratio for this image is 1000.0 / 755 = 1.3245033112582782
# If the ratios are not acknowledged to be the same,
# and Pillow attempts to adjust the width to
@@ -106,13 +108,13 @@ def test_fit_same_ratio():
@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512)))
-def test_contain(new_size):
+def test_contain(new_size) -> None:
im = hopper()
new_im = ImageOps.contain(im, new_size)
assert new_im.size == (256, 256)
-def test_contain_round():
+def test_contain_round() -> None:
im = Image.new("1", (43, 63), 1)
new_im = ImageOps.contain(im, (5, 7))
assert new_im.width == 5
@@ -130,13 +132,13 @@ def test_contain_round():
("hopper.png", (256, 256)), # square
),
)
-def test_cover(image_name, expected_size):
+def test_cover(image_name, expected_size) -> None:
with Image.open("Tests/images/" + image_name) as im:
new_im = ImageOps.cover(im, (256, 256))
assert new_im.size == expected_size
-def test_pad():
+def test_pad() -> None:
# Same ratio
im = hopper()
new_size = (im.width * 2, im.height * 2)
@@ -156,7 +158,7 @@ def test_pad():
)
-def test_pad_round():
+def test_pad_round() -> None:
im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 1))
assert new_im.load()[2, 0] == 1
@@ -166,7 +168,7 @@ def test_pad_round():
@pytest.mark.parametrize("mode", ("P", "PA"))
-def test_palette(mode):
+def test_palette(mode) -> None:
im = hopper(mode)
# Expand
@@ -180,7 +182,7 @@ def test_palette(mode):
)
-def test_pil163():
+def test_pil163() -> None:
# Division by zero in equalize if < 255 pixels in image (@PIL163)
i = hopper("RGB").resize((15, 16))
@@ -190,7 +192,7 @@ def test_pil163():
ImageOps.equalize(i.convert("RGB"))
-def test_scale():
+def test_scale() -> None:
# Test the scaling function
i = hopper("L").resize((50, 50))
@@ -208,7 +210,7 @@ def test_scale():
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
-def test_expand_palette(border):
+def test_expand_palette(border) -> None:
with Image.open("Tests/images/p_16.tga") as im:
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
@@ -234,7 +236,7 @@ def test_expand_palette(border):
assert_image_equal(im_cropped, im)
-def test_colorize_2color():
+def test_colorize_2color() -> None:
# Test the colorizing function with 2-color functionality
# Open test image (256px by 10px, black to white)
@@ -268,7 +270,7 @@ def test_colorize_2color():
)
-def test_colorize_2color_offset():
+def test_colorize_2color_offset() -> None:
# Test the colorizing function with 2-color functionality and offset
# Open test image (256px by 10px, black to white)
@@ -304,7 +306,7 @@ def test_colorize_2color_offset():
)
-def test_colorize_3color_offset():
+def test_colorize_3color_offset() -> None:
# Test the colorizing function with 3-color functionality and offset
# Open test image (256px by 10px, black to white)
@@ -357,14 +359,14 @@ def test_colorize_3color_offset():
)
-def test_exif_transpose():
+def test_exif_transpose() -> None:
exts = [".jpg"]
if features.check("webp") and features.check("webp_anim"):
exts.append(".webp")
for ext in exts:
with Image.open("Tests/images/hopper" + ext) as base_im:
- def check(orientation_im):
+ def check(orientation_im) -> None:
for im in [
orientation_im,
orientation_im.copy(),
@@ -421,7 +423,7 @@ def test_exif_transpose():
assert 0x0112 not in transposed_im.getexif()
-def test_exif_transpose_in_place():
+def test_exif_transpose_in_place() -> None:
with Image.open("Tests/images/orientation_rectangle.jpg") as im:
assert im.size == (2, 1)
assert im.getexif()[0x0112] == 8
@@ -433,7 +435,13 @@ def test_exif_transpose_in_place():
assert_image_equal(im, expected)
-def test_autocontrast_cutoff():
+def test_autocontrast_unsupported_mode() -> None:
+ im = Image.new("RGBA", (1, 1))
+ with pytest.raises(OSError):
+ ImageOps.autocontrast(im)
+
+
+def test_autocontrast_cutoff() -> None:
# Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:
@@ -444,7 +452,7 @@ def test_autocontrast_cutoff():
assert autocontrast(10) != autocontrast((1, 10))
-def test_autocontrast_mask_toy_input():
+def test_autocontrast_mask_toy_input() -> None:
# Test the mask argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:
rect_mask = Image.new("L", img.size, 0)
@@ -463,7 +471,7 @@ def test_autocontrast_mask_toy_input():
assert ImageStat.Stat(result_nomask).median == [128]
-def test_autocontrast_mask_real_input():
+def test_autocontrast_mask_real_input() -> None:
# Test the autocontrast with a rectangular mask
with Image.open("Tests/images/iptc.jpg") as img:
rect_mask = Image.new("L", img.size, 0)
@@ -490,7 +498,7 @@ def test_autocontrast_mask_real_input():
)
-def test_autocontrast_preserve_tone():
+def test_autocontrast_preserve_tone() -> None:
def autocontrast(mode, preserve_tone):
im = hopper(mode)
return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()
@@ -499,7 +507,7 @@ def test_autocontrast_preserve_tone():
assert autocontrast("L", True) == autocontrast("L", False)
-def test_autocontrast_preserve_gradient():
+def test_autocontrast_preserve_gradient() -> None:
gradient = Image.linear_gradient("L")
# test with a grayscale gradient that extends to 0,255.
@@ -525,7 +533,7 @@ def test_autocontrast_preserve_gradient():
@pytest.mark.parametrize(
"color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0))
)
-def test_autocontrast_preserve_one_color(color):
+def test_autocontrast_preserve_one_color(color) -> None:
img = Image.new("RGB", (10, 10), color)
# single color images shouldn't change
diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py
index 8837ed2a2..03302e20f 100644
--- a/Tests/test_imageops_usm.py
+++ b/Tests/test_imageops_usm.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageFilter
@@ -16,7 +18,7 @@ def test_images():
im.close()
-def test_filter_api(test_images):
+def test_filter_api(test_images) -> None:
im = test_images["im"]
test_filter = ImageFilter.GaussianBlur(2.0)
@@ -30,7 +32,7 @@ def test_filter_api(test_images):
assert i.size == (128, 128)
-def test_usm_formats(test_images):
+def test_usm_formats(test_images) -> None:
im = test_images["im"]
usm = ImageFilter.UnsharpMask
@@ -48,7 +50,7 @@ def test_usm_formats(test_images):
im.convert("YCbCr").filter(usm)
-def test_blur_formats(test_images):
+def test_blur_formats(test_images) -> None:
im = test_images["im"]
blur = ImageFilter.GaussianBlur
@@ -66,7 +68,7 @@ def test_blur_formats(test_images):
im.convert("YCbCr").filter(blur)
-def test_usm_accuracy(test_images):
+def test_usm_accuracy(test_images) -> None:
snakes = test_images["snakes"]
src = snakes.convert("RGB")
@@ -75,7 +77,7 @@ def test_usm_accuracy(test_images):
assert i.tobytes() == src.tobytes()
-def test_blur_accuracy(test_images):
+def test_blur_accuracy(test_images) -> None:
snakes = test_images["snakes"]
i = snakes.filter(ImageFilter.GaussianBlur(0.4))
diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py
index baa698bb4..545229500 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImagePalette
@@ -5,19 +9,19 @@ from PIL import Image, ImagePalette
from .helper import assert_image_equal, assert_image_equal_tofile
-def test_sanity():
+def test_sanity() -> None:
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
assert len(palette.colors) == 256
-def test_reload():
+def test_reload() -> None:
with Image.open("Tests/images/hopper.gif") as im:
original = im.copy()
im.palette.dirty = 1
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
-def test_getcolor():
+def test_getcolor() -> None:
palette = ImagePalette.ImagePalette()
assert len(palette.palette) == 0
assert len(palette.colors) == 0
@@ -44,7 +48,7 @@ def test_getcolor():
palette.getcolor("unknown")
-def test_getcolor_rgba_color_rgb_palette():
+def test_getcolor_rgba_color_rgb_palette() -> None:
palette = ImagePalette.ImagePalette("RGB")
# Opaque RGBA colors are converted
@@ -63,7 +67,7 @@ def test_getcolor_rgba_color_rgb_palette():
(255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)),
],
)
-def test_getcolor_not_special(index, palette):
+def test_getcolor_not_special(index, palette) -> None:
im = Image.new("P", (1, 1))
# Do not use transparency index as a new color
@@ -77,7 +81,7 @@ def test_getcolor_not_special(index, palette):
assert index2 not in (index, index1)
-def test_file(tmp_path):
+def test_file(tmp_path: Path) -> None:
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
f = str(tmp_path / "temp.lut")
@@ -95,7 +99,7 @@ def test_file(tmp_path):
assert p.palette == palette.tobytes()
-def test_make_linear_lut():
+def test_make_linear_lut() -> None:
# Arrange
black = 0
white = 255
@@ -111,7 +115,7 @@ def test_make_linear_lut():
assert lut[i] == i
-def test_make_linear_lut_not_yet_implemented():
+def test_make_linear_lut_not_yet_implemented() -> None:
# Update after FIXME
# Arrange
black = 1
@@ -122,7 +126,7 @@ def test_make_linear_lut_not_yet_implemented():
ImagePalette.make_linear_lut(black, white)
-def test_make_gamma_lut():
+def test_make_gamma_lut() -> None:
# Arrange
exp = 5
@@ -140,7 +144,7 @@ def test_make_gamma_lut():
assert lut[255] == 255
-def test_rawmode_valueerrors(tmp_path):
+def test_rawmode_valueerrors(tmp_path: Path) -> None:
# Arrange
palette = ImagePalette.raw("RGB", list(range(256)) * 3)
@@ -154,7 +158,7 @@ def test_rawmode_valueerrors(tmp_path):
palette.save(f)
-def test_getdata():
+def test_getdata() -> None:
# Arrange
data_in = list(range(256)) * 3
palette = ImagePalette.ImagePalette("RGB", data_in)
@@ -166,7 +170,7 @@ def test_getdata():
assert mode == "RGB"
-def test_rawmode_getdata():
+def test_rawmode_getdata() -> None:
# Arrange
data_in = list(range(256)) * 3
palette = ImagePalette.raw("RGB", data_in)
@@ -179,7 +183,7 @@ def test_rawmode_getdata():
assert data_in == data_out
-def test_2bit_palette(tmp_path):
+def test_2bit_palette(tmp_path: Path) -> None:
# issue #2258, 2 bit palettes are corrupted.
outfile = str(tmp_path / "temp.png")
@@ -191,6 +195,6 @@ def test_2bit_palette(tmp_path):
assert_image_equal_tofile(img, outfile)
-def test_invalid_palette():
+def test_invalid_palette() -> None:
with pytest.raises(OSError):
ImagePalette.load("Tests/images/hopper.jpg")
diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py
index c112cfd87..8ba745f21 100644
--- a/Tests/test_imagepath.py
+++ b/Tests/test_imagepath.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import array
import math
import struct
@@ -7,7 +9,7 @@ import pytest
from PIL import Image, ImagePath
-def test_path():
+def test_path() -> None:
p = ImagePath.Path(list(range(10)))
# sequence interface
@@ -55,7 +57,7 @@ def test_path():
ImagePath.Path((0, 1)),
),
)
-def test_path_constructors(coords):
+def test_path_constructors(coords) -> None:
# Arrange / Act
p = ImagePath.Path(coords)
@@ -73,7 +75,7 @@ def test_path_constructors(coords):
[[0.0, 1.0]],
),
)
-def test_invalid_path_constructors(coords):
+def test_invalid_path_constructors(coords) -> None:
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
@@ -91,7 +93,7 @@ def test_invalid_path_constructors(coords):
[0, 1, 2],
),
)
-def test_path_odd_number_of_coordinates(coords):
+def test_path_odd_number_of_coordinates(coords) -> None:
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
@@ -109,7 +111,7 @@ def test_path_odd_number_of_coordinates(coords):
(1, (0.0, 0.0, 0.0, 0.0)),
],
)
-def test_getbbox(coords, expected):
+def test_getbbox(coords, expected) -> None:
# Arrange
p = ImagePath.Path(coords)
@@ -117,7 +119,7 @@ def test_getbbox(coords, expected):
assert p.getbbox() == expected
-def test_getbbox_no_args():
+def test_getbbox_no_args() -> None:
# Arrange
p = ImagePath.Path([0, 1, 2, 3])
@@ -133,7 +135,7 @@ def test_getbbox_no_args():
(list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]),
],
)
-def test_map(coords, expected):
+def test_map(coords, expected) -> None:
# Arrange
p = ImagePath.Path(coords)
@@ -145,7 +147,7 @@ def test_map(coords, expected):
assert list(p) == expected
-def test_transform():
+def test_transform() -> None:
# Arrange
p = ImagePath.Path([0, 1, 2, 3])
theta = math.pi / 15
@@ -163,7 +165,7 @@ def test_transform():
]
-def test_transform_with_wrap():
+def test_transform_with_wrap() -> None:
# Arrange
p = ImagePath.Path([0, 1, 2, 3])
theta = math.pi / 15
@@ -182,7 +184,7 @@ def test_transform_with_wrap():
]
-def test_overflow_segfault():
+def test_overflow_segfault() -> None:
# Some Pythons fail getting the argument as an integer, and it falls
# through to the sequence. Seeing this on 32-bit Windows.
with pytest.raises((TypeError, MemoryError)):
@@ -196,12 +198,12 @@ def test_overflow_segfault():
class Evil:
- def __init__(self):
+ def __init__(self) -> None:
self.corrupt = Image.core.path(0x4000000000000000)
def __getitem__(self, i):
x = self.corrupt[i]
return struct.pack("dd", x[0], x[1])
- def __setitem__(self, i, x):
+ def __setitem__(self, i, x) -> None:
self.corrupt[i] = struct.unpack("dd", x)
diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py
index 2c73a2094..909f97167 100644
--- a/Tests/test_imageqt.py
+++ b/Tests/test_imageqt.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import warnings
import pytest
@@ -14,7 +16,7 @@ if ImageQt.qt_is_installed:
from PIL.ImageQt import qRgba
-def test_rgb():
+def test_rgb() -> None:
# from https://doc.qt.io/archives/qt-4.8/qcolor.html
# typedef QRgb
# An ARGB quadruplet on the format #AARRGGBB,
@@ -26,7 +28,7 @@ def test_rgb():
assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255)
- def checkrgb(r, g, b):
+ def checkrgb(r, g, b) -> None:
val = ImageQt.rgb(r, g, b)
val = val % 2**24 # drop the alpha
assert val >> 16 == r
@@ -39,7 +41,7 @@ def test_rgb():
checkrgb(0, 0, 255)
-def test_image():
+def test_image() -> None:
modes = ["1", "RGB", "RGBA", "L", "P"]
qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage
if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
@@ -53,6 +55,6 @@ def test_image():
assert_image_similar(roundtripped_im, im, 1)
-def test_closed_file():
+def test_closed_file() -> None:
with warnings.catch_warnings():
ImageQt.ImageQt("Tests/images/hopper.gif")
diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py
index 62f528332..7280dded0 100644
--- a/Tests/test_imagesequence.py
+++ b/Tests/test_imagesequence.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImageSequence, TiffImagePlugin
@@ -5,7 +9,7 @@ from PIL import Image, ImageSequence, TiffImagePlugin
from .helper import assert_image_equal, hopper, skip_unless_feature
-def test_sanity(tmp_path):
+def test_sanity(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.im")
im = hopper("RGB")
@@ -25,7 +29,7 @@ def test_sanity(tmp_path):
ImageSequence.Iterator(0)
-def test_iterator():
+def test_iterator() -> None:
with Image.open("Tests/images/multipage.tiff") as im:
i = ImageSequence.Iterator(im)
for index in range(0, im.n_frames):
@@ -36,14 +40,14 @@ def test_iterator():
next(i)
-def test_iterator_min_frame():
+def test_iterator_min_frame() -> None:
with Image.open("Tests/images/hopper.psd") as im:
i = ImageSequence.Iterator(im)
for index in range(1, im.n_frames):
assert i[index] == next(i)
-def _test_multipage_tiff():
+def _test_multipage_tiff() -> None:
with Image.open("Tests/images/multipage.tiff") as im:
for index, frame in enumerate(ImageSequence.Iterator(im)):
frame.load()
@@ -51,18 +55,18 @@ def _test_multipage_tiff():
frame.convert("RGB")
-def test_tiff():
+def test_tiff() -> None:
_test_multipage_tiff()
@skip_unless_feature("libtiff")
-def test_libtiff():
+def test_libtiff() -> None:
TiffImagePlugin.READ_LIBTIFF = True
_test_multipage_tiff()
TiffImagePlugin.READ_LIBTIFF = False
-def test_consecutive():
+def test_consecutive() -> None:
with Image.open("Tests/images/multipage.tiff") as im:
first_frame = None
for frame in ImageSequence.Iterator(im):
@@ -73,7 +77,7 @@ def test_consecutive():
break
-def test_palette_mmap():
+def test_palette_mmap() -> None:
# Using mmap in ImageFile can require to reload the palette.
with Image.open("Tests/images/multipage-mmap.tiff") as im:
color1 = im.getpalette()[:3]
@@ -82,7 +86,7 @@ def test_palette_mmap():
assert color1 == color2
-def test_all_frames():
+def test_all_frames() -> None:
# Test a single image
with Image.open("Tests/images/iss634.gif") as im:
ims = ImageSequence.all_frames(im)
diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py
index e54372b60..f7269d45b 100644
--- a/Tests/test_imageshow.py
+++ b/Tests/test_imageshow.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageShow
@@ -5,12 +7,12 @@ from PIL import Image, ImageShow
from .helper import hopper, is_win32, on_ci
-def test_sanity():
+def test_sanity() -> None:
dir(Image)
dir(ImageShow)
-def test_register():
+def test_register() -> None:
# Test registering a viewer that is not a class
ImageShow.register("not a class")
@@ -22,9 +24,9 @@ def test_register():
"order",
[-1, 0],
)
-def test_viewer_show(order):
+def test_viewer_show(order) -> None:
class TestViewer(ImageShow.Viewer):
- def show_image(self, image, **options):
+ def show_image(self, image, **options) -> bool:
self.methodCalled = True
return True
@@ -46,12 +48,12 @@ def test_viewer_show(order):
reason="Only run on CIs; hangs on Windows CIs",
)
@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
-def test_show(mode):
+def test_show(mode) -> None:
im = hopper(mode)
assert ImageShow.show(im)
-def test_show_without_viewers():
+def test_show_without_viewers() -> None:
viewers = ImageShow._viewers
ImageShow._viewers = []
@@ -61,7 +63,7 @@ def test_show_without_viewers():
ImageShow._viewers = viewers
-def test_viewer():
+def test_viewer() -> None:
viewer = ImageShow.Viewer()
assert viewer.get_format(None) is None
@@ -71,21 +73,21 @@ def test_viewer():
@pytest.mark.parametrize("viewer", ImageShow._viewers)
-def test_viewers(viewer):
+def test_viewers(viewer) -> None:
try:
viewer.get_command("test.jpg")
except NotImplementedError:
pass
-def test_ipythonviewer():
+def test_ipythonviewer() -> None:
pytest.importorskip("IPython", reason="IPython not installed")
for viewer in ImageShow._viewers:
if isinstance(viewer, ImageShow.IPythonViewer):
test_viewer = viewer
break
else:
- assert False
+ pytest.fail()
im = hopper()
assert test_viewer.show(im) == 1
diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py
index b3b5db13f..b1c1306c1 100644
--- a/Tests/test_imagestat.py
+++ b/Tests/test_imagestat.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image, ImageStat
@@ -5,7 +7,7 @@ from PIL import Image, ImageStat
from .helper import hopper
-def test_sanity():
+def test_sanity() -> None:
im = hopper()
st = ImageStat.Stat(im)
@@ -29,7 +31,7 @@ def test_sanity():
ImageStat.Stat(1)
-def test_hopper():
+def test_hopper() -> None:
im = hopper()
st = ImageStat.Stat(im)
@@ -42,7 +44,7 @@ def test_hopper():
assert st.sum[2] == 1563008
-def test_constant():
+def test_constant() -> None:
im = Image.new("L", (128, 128), 128)
st = ImageStat.Stat(im)
diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py
index a0c9574ba..a216bd21d 100644
--- a/Tests/test_imagetk.py
+++ b/Tests/test_imagetk.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -21,7 +23,7 @@ TK_MODES = ("1", "L", "P", "RGB", "RGBA")
pytestmark = pytest.mark.skipif(not HAS_TK, reason="Tk not installed")
-def setup_module():
+def setup_module() -> None:
try:
# setup tk
tk.Frame()
@@ -32,7 +34,7 @@ def setup_module():
pytest.skip(f"TCL Error: {v}")
-def test_kw():
+def test_kw() -> None:
TEST_JPG = "Tests/images/hopper.jpg"
TEST_PNG = "Tests/images/hopper.png"
with Image.open(TEST_JPG) as im1:
@@ -55,7 +57,7 @@ def test_kw():
@pytest.mark.parametrize("mode", TK_MODES)
-def test_photoimage(mode):
+def test_photoimage(mode) -> None:
# test as image:
im = hopper(mode)
@@ -69,7 +71,7 @@ def test_photoimage(mode):
assert_image_equal(reloaded, im.convert("RGBA"))
-def test_photoimage_apply_transparency():
+def test_photoimage_apply_transparency() -> None:
with Image.open("Tests/images/pil123p.png") as im:
im_tk = ImageTk.PhotoImage(im)
reloaded = ImageTk.getimage(im_tk)
@@ -77,7 +79,7 @@ def test_photoimage_apply_transparency():
@pytest.mark.parametrize("mode", TK_MODES)
-def test_photoimage_blank(mode):
+def test_photoimage_blank(mode) -> None:
# test a image using mode/size:
im_tk = ImageTk.PhotoImage(mode, (100, 100))
@@ -89,7 +91,7 @@ def test_photoimage_blank(mode):
assert_image_equal(reloaded.convert(mode), im)
-def test_bitmapimage():
+def test_bitmapimage() -> None:
im = hopper("1")
# this should not crash
diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py
index 5e489284f..b43c31b52 100644
--- a/Tests/test_imagewin.py
+++ b/Tests/test_imagewin.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import ImageWin
@@ -6,10 +8,10 @@ from .helper import hopper, is_win32
class TestImageWin:
- def test_sanity(self):
+ def test_sanity(self) -> None:
dir(ImageWin)
- def test_hdc(self):
+ def test_hdc(self) -> None:
# Arrange
dc = 50
@@ -20,7 +22,7 @@ class TestImageWin:
# Assert
assert dc2 == 50
- def test_hwnd(self):
+ def test_hwnd(self) -> None:
# Arrange
wnd = 50
@@ -34,7 +36,7 @@ class TestImageWin:
@pytest.mark.skipif(not is_win32(), reason="Windows only")
class TestImageWinDib:
- def test_dib_image(self):
+ def test_dib_image(self) -> None:
# Arrange
im = hopper()
@@ -44,7 +46,7 @@ class TestImageWinDib:
# Assert
assert dib.size == im.size
- def test_dib_mode_string(self):
+ def test_dib_mode_string(self) -> None:
# Arrange
mode = "RGBA"
size = (128, 128)
@@ -55,7 +57,7 @@ class TestImageWinDib:
# Assert
assert dib.size == (128, 128)
- def test_dib_paste(self):
+ def test_dib_paste(self) -> None:
# Arrange
im = hopper()
@@ -69,7 +71,7 @@ class TestImageWinDib:
# Assert
assert dib.size == (128, 128)
- def test_dib_paste_bbox(self):
+ def test_dib_paste_bbox(self) -> None:
# Arrange
im = hopper()
bbox = (0, 0, 10, 10)
@@ -84,7 +86,7 @@ class TestImageWinDib:
# Assert
assert dib.size == (128, 128)
- def test_dib_frombytes_tobytes_roundtrip(self):
+ def test_dib_frombytes_tobytes_roundtrip(self) -> None:
# Arrange
# Make two different DIB images
im = hopper()
diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py
index df1305655..c7f633e62 100644
--- a/Tests/test_imagewin_pointers.py
+++ b/Tests/test_imagewin_pointers.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
from PIL import Image, ImageWin
@@ -81,7 +84,7 @@ if is_win32():
memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage)
return bytearray(buf)
- def test_pointer(tmp_path):
+ def test_pointer(tmp_path: Path) -> None:
im = hopper()
(width, height) = im.size
opath = str(tmp_path / "temp.png")
diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py
index f6818be46..31548bbc9 100644
--- a/Tests/test_lib_image.py
+++ b/Tests/test_lib_image.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
-def test_setmode():
+def test_setmode() -> None:
im = Image.new("L", (1, 1), 255)
im.im.setmode("1")
assert im.im.getpixel((0, 0)) == 255
diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py
index 2a4d9acf4..c8d6d33d2 100644
--- a/Tests/test_lib_pack.py
+++ b/Tests/test_lib_pack.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import sys
import pytest
@@ -8,7 +10,7 @@ X = 255
class TestLibPack:
- def assert_pack(self, mode, rawmode, data, *pixels):
+ def assert_pack(self, mode, rawmode, data, *pixels) -> None:
"""
data - either raw bytes with data or just number of bytes in rawmode.
"""
@@ -22,7 +24,7 @@ class TestLibPack:
assert data == im.tobytes("raw", rawmode)
- def test_1(self):
+ def test_1(self) -> None:
self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X)
self.assert_pack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0)
self.assert_pack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0)
@@ -35,29 +37,29 @@ class TestLibPack:
self.assert_pack("1", "L", b"\xff\x00\x00\xff\x00\x00", X, 0, 0, X, 0, 0)
- def test_L(self):
+ def test_L(self) -> None:
self.assert_pack("L", "L", 1, 1, 2, 3, 4)
self.assert_pack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175)
self.assert_pack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175)
- def test_LA(self):
+ def test_LA(self) -> None:
self.assert_pack("LA", "LA", 2, (1, 2), (3, 4), (5, 6))
self.assert_pack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6))
- def test_La(self):
+ def test_La(self) -> None:
self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6))
- def test_P(self):
+ def test_P(self) -> None:
self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0)
self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0)
self.assert_pack("P", "P;4", b"\x02\xef", 0, 2, 14, 15)
self.assert_pack("P", "P", 1, 1, 2, 3, 4)
- def test_PA(self):
+ def test_PA(self) -> None:
self.assert_pack("PA", "PA", 2, (1, 2), (3, 4), (5, 6))
self.assert_pack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6))
- def test_RGB(self):
+ def test_RGB(self) -> None:
self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
self.assert_pack(
"RGB", "RGBX", b"\x01\x02\x03\xff\x05\x06\x07\xff", (1, 2, 3), (5, 6, 7)
@@ -77,7 +79,7 @@ class TestLibPack:
self.assert_pack("RGB", "G", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9))
self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3))
- def test_RGBA(self):
+ def test_RGBA(self) -> None:
self.assert_pack("RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12))
self.assert_pack(
"RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)
@@ -99,12 +101,12 @@ class TestLibPack:
self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9))
self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3))
- def test_RGBa(self):
+ def test_RGBa(self) -> None:
self.assert_pack("RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12))
self.assert_pack("RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12))
self.assert_pack("RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9))
- def test_RGBX(self):
+ def test_RGBX(self) -> None:
self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12))
self.assert_pack(
"RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)
@@ -132,7 +134,7 @@ class TestLibPack:
self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9))
self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3))
- def test_CMYK(self):
+ def test_CMYK(self) -> None:
self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12))
self.assert_pack(
"CMYK",
@@ -147,7 +149,7 @@ class TestLibPack:
)
self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3))
- def test_YCbCr(self):
+ def test_YCbCr(self) -> None:
self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9))
self.assert_pack(
@@ -170,19 +172,19 @@ class TestLibPack:
self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9))
self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9))
- def test_LAB(self):
+ def test_LAB(self) -> None:
self.assert_pack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137))
self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9))
self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9))
self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3))
- def test_HSV(self):
+ def test_HSV(self) -> None:
self.assert_pack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
self.assert_pack("HSV", "H", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9))
self.assert_pack("HSV", "S", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9))
self.assert_pack("HSV", "V", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3))
- def test_I(self):
+ def test_I(self) -> None:
self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304)
self.assert_pack(
"I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999
@@ -207,10 +209,10 @@ class TestLibPack:
0x01000083,
)
- def test_I16(self):
+ def test_I16(self) -> None:
self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605)
- def test_F_float(self):
+ def test_F_float(self) -> None:
self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34)
if sys.byteorder == "little":
@@ -226,7 +228,7 @@ class TestLibPack:
class TestLibUnpack:
- def assert_unpack(self, mode, rawmode, data, *pixels):
+ def assert_unpack(self, mode, rawmode, data, *pixels) -> None:
"""
data - either raw bytes with data or just number of bytes in rawmode.
"""
@@ -239,7 +241,7 @@ class TestLibUnpack:
for x, pixel in enumerate(pixels):
assert pixel == im.getpixel((x, 0))
- def test_1(self):
+ def test_1(self) -> None:
self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X)
self.assert_unpack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0)
self.assert_unpack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0)
@@ -252,7 +254,7 @@ class TestLibUnpack:
self.assert_unpack("1", "1;8", b"\x00\x01\x02\xff", 0, X, X, X)
- def test_L(self):
+ def test_L(self) -> None:
self.assert_unpack("L", "L;2", b"\xe4", 255, 170, 85, 0)
self.assert_unpack("L", "L;2I", b"\xe4", 0, 85, 170, 255)
self.assert_unpack("L", "L;2R", b"\xe4", 0, 170, 85, 255)
@@ -271,14 +273,14 @@ class TestLibUnpack:
self.assert_unpack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175)
self.assert_unpack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175)
- def test_LA(self):
+ def test_LA(self) -> None:
self.assert_unpack("LA", "LA", 2, (1, 2), (3, 4), (5, 6))
self.assert_unpack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6))
- def test_La(self):
+ def test_La(self) -> None:
self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6))
- def test_P(self):
+ def test_P(self) -> None:
self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0)
self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0)
# erroneous?
@@ -289,11 +291,11 @@ class TestLibUnpack:
self.assert_unpack("P", "P", 1, 1, 2, 3, 4)
self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32)
- def test_PA(self):
+ def test_PA(self) -> None:
self.assert_unpack("PA", "PA", 2, (1, 2), (3, 4), (5, 6))
self.assert_unpack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6))
- def test_RGB(self):
+ def test_RGB(self) -> None:
self.assert_unpack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
self.assert_unpack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9))
self.assert_unpack("RGB", "RGB;R", 3, (128, 64, 192), (32, 160, 96))
@@ -344,14 +346,14 @@ class TestLibUnpack:
"RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233)
)
- def test_BGR(self):
+ def test_BGR(self) -> None:
self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8))
self.assert_unpack(
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
)
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
- def test_RGBA(self):
+ def test_RGBA(self) -> None:
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
self.assert_unpack(
"RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11)
@@ -520,7 +522,7 @@ class TestLibUnpack:
"RGBA", "A;16N", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5)
)
- def test_RGBa(self):
+ def test_RGBa(self) -> None:
self.assert_unpack(
"RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)
)
@@ -534,7 +536,7 @@ class TestLibUnpack:
"RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)
)
- def test_RGBX(self):
+ def test_RGBX(self) -> None:
self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X))
self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X))
self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X))
@@ -579,7 +581,7 @@ class TestLibUnpack:
self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0))
self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3))
- def test_CMYK(self):
+ def test_CMYK(self) -> None:
self.assert_unpack(
"CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)
)
@@ -617,25 +619,25 @@ class TestLibUnpack:
"CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252)
)
- def test_YCbCr(self):
+ def test_YCbCr(self) -> None:
self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9))
self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11))
self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11))
- def test_LAB(self):
+ def test_LAB(self) -> None:
self.assert_unpack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137))
self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0))
self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0))
self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3))
- def test_HSV(self):
+ def test_HSV(self) -> None:
self.assert_unpack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
self.assert_unpack("HSV", "H", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0))
self.assert_unpack("HSV", "S", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0))
self.assert_unpack("HSV", "V", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3))
- def test_I(self):
+ def test_I(self) -> None:
self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04)
self.assert_unpack("I", "I;8S", b"\x01\x83", 1, -125)
self.assert_unpack("I", "I;16", 2, 0x0201, 0x0403)
@@ -676,7 +678,7 @@ class TestLibUnpack:
0x01000083,
)
- def test_F_int(self):
+ def test_F_int(self) -> None:
self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04)
self.assert_unpack("F", "F;8S", b"\x01\x83", 1, -125)
self.assert_unpack("F", "F;16", 2, 0x0201, 0x0403)
@@ -715,7 +717,7 @@ class TestLibUnpack:
16777348,
)
- def test_F_float(self):
+ def test_F_float(self) -> None:
self.assert_unpack(
"F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34
)
@@ -766,7 +768,7 @@ class TestLibUnpack:
-1234.5,
)
- def test_I16(self):
+ def test_I16(self) -> None:
self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605)
self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506)
self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506)
@@ -783,7 +785,7 @@ class TestLibUnpack:
self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506)
self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506)
- def test_CMYK16(self):
+ def test_CMYK16(self) -> None:
self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
self.assert_unpack("CMYK", "CMYK;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
if sys.byteorder == "little":
@@ -791,7 +793,7 @@ class TestLibUnpack:
else:
self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15))
- def test_value_error(self):
+ def test_value_error(self) -> None:
with pytest.raises(ValueError):
self.assert_unpack("L", "L", 0, 0)
with pytest.raises(ValueError):
diff --git a/Tests/test_locale.py b/Tests/test_locale.py
index 7a07fbbe0..1c8b84a2b 100644
--- a/Tests/test_locale.py
+++ b/Tests/test_locale.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import locale
import pytest
@@ -22,7 +24,7 @@ from PIL import Image
path = "Tests/images/hopper.jpg"
-def test_sanity():
+def test_sanity() -> None:
with Image.open(path):
pass
try:
diff --git a/Tests/test_main.py b/Tests/test_main.py
index 46ff63c4e..46259f1dc 100644
--- a/Tests/test_main.py
+++ b/Tests/test_main.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
import os
import subprocess
import sys
-def test_main():
+def test_main() -> None:
out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8")
lines = out.splitlines()
assert lines[0] == "-" * 68
diff --git a/Tests/test_map.py b/Tests/test_map.py
index d816bddaf..93140f6e5 100644
--- a/Tests/test_map.py
+++ b/Tests/test_map.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import sys
import pytest
@@ -5,7 +7,7 @@ import pytest
from PIL import Image
-def test_overflow():
+def test_overflow() -> None:
# There is the potential to overflow comparisons in map.c
# if there are > SIZE_MAX bytes in the image or if
# the file encodes an offset that makes
@@ -23,7 +25,7 @@ def test_overflow():
Image.MAX_IMAGE_PIXELS = max_pixels
-def test_tobytes():
+def test_tobytes() -> None:
# Note that this image triggers the decompression bomb warning:
max_pixels = Image.MAX_IMAGE_PIXELS
Image.MAX_IMAGE_PIXELS = None
@@ -37,7 +39,7 @@ def test_tobytes():
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
-def test_ysize():
+def test_ysize() -> None:
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
# Should not raise 'Integer overflow in ysize'
diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py
index 1786dba38..f2540bb46 100644
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -7,7 +11,7 @@ from .helper import hopper
original = hopper().resize((32, 32)).convert("I")
-def verify(im1):
+def verify(im1) -> None:
im2 = original.copy()
assert im1.size == im2.size
pix1 = im1.load()
@@ -23,7 +27,7 @@ def verify(im1):
@pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I"))
-def test_basic(tmp_path, mode):
+def test_basic(tmp_path: Path, mode) -> None:
# PIL 1.1 has limited support for 16-bit image data. Check that
# create/copy/transform and save works as expected.
@@ -73,7 +77,7 @@ def test_basic(tmp_path, mode):
assert im_in.getpixel((0, 0)) == min(512, maximum)
-def test_tobytes():
+def test_tobytes() -> None:
def tobytes(mode):
return Image.new(mode, (1, 1), 1).tobytes()
@@ -85,7 +89,7 @@ def test_tobytes():
assert tobytes("I") == b"\x01\x00\x00\x00"[::order]
-def test_convert():
+def test_convert() -> None:
im = original.copy()
for mode in ("I;16", "I;16B", "I;16N"):
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
index 147f94a71..6ba95c2d7 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import warnings
import pytest
@@ -11,8 +13,8 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
TEST_IMAGE_SIZE = (10, 10)
-def test_numpy_to_image():
- def to_image(dtype, bands=1, boolean=0):
+def test_numpy_to_image() -> None:
+ def to_image(dtype, bands: int = 1, boolean: int = 0):
if bands == 1:
if boolean:
data = [0, 255] * 50
@@ -80,7 +82,7 @@ def test_numpy_to_image():
# Based on an erring example at
# https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function
-def test_3d_array():
+def test_3d_array() -> None:
size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1])
a = numpy.ones(size, dtype=numpy.uint8)
assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE)
@@ -92,12 +94,12 @@ def test_3d_array():
assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE)
-def test_1d_array():
+def test_1d_array() -> None:
a = numpy.ones(5, dtype=numpy.uint8)
assert_image(Image.fromarray(a), "L", (1, 5))
-def _test_img_equals_nparray(img, np):
+def _test_img_equals_nparray(img, np) -> None:
assert len(np.shape) >= 2
np_size = np.shape[1], np.shape[0]
assert img.size == np_size
@@ -107,14 +109,14 @@ def _test_img_equals_nparray(img, np):
assert_deep_equal(px[x, y], np[y, x])
-def test_16bit():
+def test_16bit() -> None:
with Image.open("Tests/images/16bit.cropped.tif") as img:
np_img = numpy.array(img)
_test_img_equals_nparray(img, np_img)
assert np_img.dtype == numpy.dtype(" None:
# Test that 1-bit arrays convert to numpy and back
# See: https://github.com/python-pillow/Pillow/issues/350
arr = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], "u1")
@@ -124,7 +126,7 @@ def test_1bit():
numpy.testing.assert_array_equal(arr, arr_back)
-def test_save_tiff_uint16():
+def test_save_tiff_uint16() -> None:
# Tests that we're getting the pixel value in the right byte order.
pixel_value = 0x1234
a = numpy.array(
@@ -155,7 +157,7 @@ def test_save_tiff_uint16():
("HSV", numpy.uint8),
),
)
-def test_to_array(mode, dtype):
+def test_to_array(mode, dtype) -> None:
img = hopper(mode)
# Resize to non-square
@@ -167,7 +169,7 @@ def test_to_array(mode, dtype):
assert np_img.dtype == dtype
-def test_point_lut():
+def test_point_lut() -> None:
# See https://github.com/python-pillow/Pillow/issues/439
data = list(range(256)) * 3
@@ -178,7 +180,7 @@ def test_point_lut():
im.point(lut)
-def test_putdata():
+def test_putdata() -> None:
# Shouldn't segfault
# See https://github.com/python-pillow/Pillow/issues/1008
@@ -205,12 +207,12 @@ def test_putdata():
numpy.float64,
),
)
-def test_roundtrip_eye(dtype):
+def test_roundtrip_eye(dtype) -> None:
arr = numpy.eye(10, dtype=dtype)
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))
-def test_zero_size():
+def test_zero_size() -> None:
# Shouldn't cause floating point exception
# See https://github.com/python-pillow/Pillow/issues/2259
@@ -220,13 +222,13 @@ def test_zero_size():
@skip_unless_feature("libtiff")
-def test_load_first():
+def test_load_first() -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im:
a = numpy.array(im)
assert a.shape == (88, 590)
-def test_bool():
+def test_bool() -> None:
# https://github.com/python-pillow/Pillow/issues/2044
a = numpy.zeros((10, 2), dtype=bool)
a[0][0] = True
@@ -235,7 +237,7 @@ def test_bool():
assert im2.getdata()[0] == 255
-def test_no_resource_warning_for_numpy_array():
+def test_no_resource_warning_for_numpy_array() -> None:
# https://github.com/python-pillow/Pillow/issues/835
# Arrange
from numpy import array
diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py
index 105a838d9..f6b12cb20 100644
--- a/Tests/test_pdfparser.py
+++ b/Tests/test_pdfparser.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import time
import pytest
@@ -17,14 +19,14 @@ from PIL.PdfParser import (
)
-def test_text_encode_decode():
+def test_text_encode_decode() -> None:
assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c"
assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc"
assert decode_text(b"abc") == "abc"
assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD"
-def test_indirect_refs():
+def test_indirect_refs() -> None:
assert IndirectReference(1, 2) == IndirectReference(1, 2)
assert IndirectReference(1, 2) != IndirectReference(1, 3)
assert IndirectReference(1, 2) != IndirectObjectDef(1, 2)
@@ -35,7 +37,7 @@ def test_indirect_refs():
assert IndirectObjectDef(1, 2) != (1, 2)
-def test_parsing():
+def test_parsing() -> None:
assert PdfParser.interpret_name(b"Name#23Hash") == b"Name#Hash"
assert PdfParser.interpret_name(b"Name#23Hash", as_text=True) == "Name#Hash"
assert PdfParser.get_value(b"1 2 R ", 0) == (IndirectReference(1, 2), 5)
@@ -93,7 +95,7 @@ def test_parsing():
assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value
-def test_pdf_repr():
+def test_pdf_repr() -> None:
assert bytes(IndirectReference(1, 2)) == b"1 2 R"
assert bytes(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj"
assert bytes(PdfName(b"Name#Hash")) == b"/Name#23Hash"
@@ -119,7 +121,7 @@ def test_pdf_repr():
assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>"
-def test_duplicate_xref_entry():
+def test_duplicate_xref_entry() -> None:
pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf")
assert pdf.xref_table.existing_entries[6][0] == 1197
pdf.close()
diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py
index 1c5d482bd..560cdbd35 100644
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
import pickle
+from pathlib import Path
import pytest
@@ -10,7 +13,7 @@ FONT_SIZE = 20
FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
-def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode):
+def helper_pickle_file(tmp_path: Path, pickle, protocol, test_file, mode) -> None:
# Arrange
with Image.open(test_file) as im:
filename = str(tmp_path / "temp.pkl")
@@ -27,7 +30,7 @@ def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode):
assert im == loaded_im
-def helper_pickle_string(pickle, protocol, test_file, mode):
+def helper_pickle_string(pickle, protocol, test_file, mode) -> None:
with Image.open(test_file) as im:
if mode:
im = im.convert(mode)
@@ -61,13 +64,13 @@ def helper_pickle_string(pickle, protocol, test_file, mode):
],
)
@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1))
-def test_pickle_image(tmp_path, test_file, test_mode, protocol):
+def test_pickle_image(tmp_path: Path, test_file, test_mode, protocol) -> None:
# Act / Assert
helper_pickle_string(pickle, protocol, test_file, test_mode)
helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode)
-def test_pickle_la_mode_with_palette(tmp_path):
+def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange
filename = str(tmp_path / "temp.pkl")
with Image.open("Tests/images/hopper.jpg") as im:
@@ -86,7 +89,7 @@ def test_pickle_la_mode_with_palette(tmp_path):
@skip_unless_feature("webp")
-def test_pickle_tell():
+def test_pickle_tell() -> None:
# Arrange
with Image.open("Tests/images/hopper.webp") as image:
# Act: roundtrip
@@ -96,7 +99,7 @@ def test_pickle_tell():
assert unpickled_image.tell() == 0
-def helper_assert_pickled_font_images(font1, font2):
+def helper_assert_pickled_font_images(font1, font2) -> None:
# Arrange
im1 = Image.new(mode="RGBA", size=(300, 100))
im2 = Image.new(mode="RGBA", size=(300, 100))
@@ -114,7 +117,7 @@ def helper_assert_pickled_font_images(font1, font2):
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
-def test_pickle_font_string(protocol):
+def test_pickle_font_string(protocol) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
@@ -128,7 +131,7 @@ def test_pickle_font_string(protocol):
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
-def test_pickle_font_file(tmp_path, protocol):
+def test_pickle_font_file(tmp_path: Path, protocol) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
filename = str(tmp_path / "temp.pkl")
diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py
index e74d79828..797539f35 100644
--- a/Tests/test_psdraw.py
+++ b/Tests/test_psdraw.py
@@ -1,13 +1,16 @@
+from __future__ import annotations
+
import os
import sys
from io import BytesIO
+from pathlib import Path
import pytest
from PIL import Image, PSDraw
-def _create_document(ps):
+def _create_document(ps) -> None:
title = "hopper"
box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points
@@ -29,7 +32,7 @@ def _create_document(ps):
ps.end_document()
-def test_draw_postscript(tmp_path):
+def test_draw_postscript(tmp_path: Path) -> None:
# Based on Pillow tutorial, but there is no textsize:
# https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript
@@ -47,7 +50,7 @@ def test_draw_postscript(tmp_path):
@pytest.mark.parametrize("buffer", (True, False))
-def test_stdout(buffer):
+def test_stdout(buffer) -> None:
# Temporarily redirect stdout
old_stdout = sys.stdout
diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py
index aa05c2cfd..c2f7fe22e 100644
--- a/Tests/test_pyroma.py
+++ b/Tests/test_pyroma.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import __version__
@@ -5,7 +7,7 @@ from PIL import __version__
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
-def test_pyroma():
+def test_pyroma() -> None:
# Arrange
data = pyroma.projectdata.get_data(".")
diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py
index 5d2e41212..7d6c0a8cb 100644
--- a/Tests/test_qt_image_qapplication.py
+++ b/Tests/test_qt_image_qapplication.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import ImageQt
@@ -17,7 +21,7 @@ if ImageQt.qt_is_installed:
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
class Example(QWidget):
- def __init__(self):
+ def __init__(self) -> None:
super().__init__()
img = hopper().resize((1000, 1000))
@@ -33,14 +37,14 @@ if ImageQt.qt_is_installed:
lbl.setPixmap(pixmap1.copy())
-def roundtrip(expected):
+def roundtrip(expected) -> None:
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
# Qt saves all pixmaps as rgb
assert_image_similar(result, expected.convert("RGB"), 1)
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
-def test_sanity(tmp_path):
+def test_sanity(tmp_path: Path) -> None:
# Segfault test
app = QApplication([])
ex = Example()
diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py
index 95c13ba75..a222a7d71 100644
--- a/Tests/test_qt_image_toqimage.py
+++ b/Tests/test_qt_image_toqimage.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import ImageQt
@@ -13,7 +17,7 @@ if ImageQt.qt_is_installed:
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1"))
-def test_sanity(mode, tmp_path):
+def test_sanity(mode, tmp_path: Path) -> None:
src = hopper(mode)
data = ImageQt.toqimage(src)
diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py
index b5f9d4424..9442801d0 100644
--- a/Tests/test_sgi_crash.py
+++ b/Tests/test_sgi_crash.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from PIL import Image
@@ -19,7 +21,7 @@ from PIL import Image
"Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi",
],
)
-def test_crashes(test_file):
+def test_crashes(test_file) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(OSError):
diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py
index d25d42dfc..3db0660ea 100644
--- a/Tests/test_shell_injection.py
+++ b/Tests/test_shell_injection.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
import shutil
+from pathlib import Path
import pytest
@@ -14,7 +17,7 @@ test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&")
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
class TestShellInjection:
- def assert_save_filename_check(self, tmp_path, src_img, save_func):
+ def assert_save_filename_check(self, tmp_path: Path, src_img, save_func) -> None:
for filename in test_filenames:
dest_file = str(tmp_path / filename)
save_func(src_img, 0, dest_file)
@@ -23,7 +26,7 @@ class TestShellInjection:
im.load()
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
- def test_load_djpeg_filename(self, tmp_path):
+ def test_load_djpeg_filename(self, tmp_path: Path) -> None:
for filename in test_filenames:
src_file = str(tmp_path / filename)
shutil.copy(TEST_JPG, src_file)
@@ -32,18 +35,18 @@ class TestShellInjection:
im.load_djpeg()
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
- def test_save_cjpeg_filename(self, tmp_path):
+ def test_save_cjpeg_filename(self, tmp_path: Path) -> None:
with Image.open(TEST_JPG) as im:
self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg)
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
- def test_save_netpbm_filename_bmp_mode(self, tmp_path):
+ def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im:
im = im.convert("RGB")
self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm)
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
- def test_save_netpbm_filename_l_mode(self, tmp_path):
+ def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im:
im = im.convert("L")
self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm)
diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py
index 143765b8e..64e781cba 100644
--- a/Tests/test_tiff_crashes.py
+++ b/Tests/test_tiff_crashes.py
@@ -10,6 +10,7 @@
# the output should be empty. There may be Python issues
# in the valgrind especially if run in a debug Python
# version.
+from __future__ import annotations
import pytest
diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py
index 6e3fcec90..536854523 100644
--- a/Tests/test_tiff_ifdrational.py
+++ b/Tests/test_tiff_ifdrational.py
@@ -1,4 +1,7 @@
+from __future__ import annotations
+
from fractions import Fraction
+from pathlib import Path
from PIL import Image, TiffImagePlugin, features
from PIL.TiffImagePlugin import IFDRational
@@ -6,14 +9,14 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import hopper
-def _test_equal(num, denom, target):
+def _test_equal(num, denom, target) -> None:
t = IFDRational(num, denom)
assert target == t
assert t == target
-def test_sanity():
+def test_sanity() -> None:
_test_equal(1, 1, 1)
_test_equal(1, 1, Fraction(1, 1))
@@ -29,13 +32,13 @@ def test_sanity():
_test_equal(7, 5, 1.4)
-def test_ranges():
+def test_ranges() -> None:
for num in range(1, 10):
for denom in range(1, 10):
assert IFDRational(num, denom) == IFDRational(num, denom)
-def test_nonetype():
+def test_nonetype() -> None:
# Fails if the _delegate function doesn't return a valid function
xres = IFDRational(72)
@@ -49,7 +52,7 @@ def test_nonetype():
assert xres and yres
-def test_ifd_rational_save(tmp_path):
+def test_ifd_rational_save(tmp_path: Path) -> None:
methods = (True, False)
if not features.check("libtiff"):
methods = (False,)
diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py
index 720926e53..d55ceb4be 100644
--- a/Tests/test_uploader.py
+++ b/Tests/test_uploader.py
@@ -1,13 +1,15 @@
+from __future__ import annotations
+
from .helper import assert_image_equal, assert_image_similar, hopper
-def check_upload_equal():
+def check_upload_equal() -> None:
result = hopper("P").convert("RGB")
target = hopper("RGB")
assert_image_equal(result, target)
-def check_upload_similar():
+def check_upload_similar() -> None:
result = hopper("P").convert("RGB")
target = hopper("RGB")
assert_image_similar(result, target, 0)
diff --git a/Tests/test_util.py b/Tests/test_util.py
index 9efbdd1f3..b47ca8827 100644
--- a/Tests/test_util.py
+++ b/Tests/test_util.py
@@ -1,9 +1,13 @@
+from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import _util
-def test_is_path():
+def test_is_path() -> None:
# Arrange
fp = "filename.ext"
@@ -14,7 +18,7 @@ def test_is_path():
assert it_is
-def test_path_obj_is_path():
+def test_path_obj_is_path() -> None:
# Arrange
from pathlib import Path
@@ -27,7 +31,7 @@ def test_path_obj_is_path():
assert it_is
-def test_is_not_path(tmp_path):
+def test_is_not_path(tmp_path: Path) -> None:
# Arrange
with (tmp_path / "temp.ext").open("w") as fp:
pass
@@ -39,7 +43,7 @@ def test_is_not_path(tmp_path):
assert not it_is_not
-def test_is_directory():
+def test_is_directory() -> None:
# Arrange
directory = "Tests"
@@ -50,7 +54,7 @@ def test_is_directory():
assert it_is
-def test_is_not_directory():
+def test_is_not_directory() -> None:
# Arrange
text = "abc"
@@ -61,11 +65,11 @@ def test_is_not_directory():
assert not it_is_not
-def test_deferred_error():
+def test_deferred_error() -> None:
# Arrange
# Act
- thing = _util.DeferredError(ValueError("Some error text"))
+ thing = _util.DeferredError.new(ValueError("Some error text"))
# Assert
with pytest.raises(ValueError):
diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py
index 5bd9bacdb..626fe427c 100644
--- a/Tests/test_webp_leaks.py
+++ b/Tests/test_webp_leaks.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from io import BytesIO
from PIL import Image
@@ -12,11 +14,11 @@ class TestWebPLeaks(PillowLeakTestCase):
mem_limit = 3 * 1024 # kb
iterations = 100
- def test_leak_load(self):
+ def test_leak_load(self) -> None:
with open(test_file, "rb") as f:
im_data = f.read()
- def core():
+ def core() -> None:
with Image.open(BytesIO(im_data)) as im:
im.load()
diff --git a/_custom_build/backend.py b/_custom_build/backend.py
index 9b3265a94..d1537b809 100644
--- a/_custom_build/backend.py
+++ b/_custom_build/backend.py
@@ -1,6 +1,8 @@
+from __future__ import annotations
+
import sys
-from setuptools.build_meta import * # noqa: F401, F403
+from setuptools.build_meta import * # noqa: F403
from setuptools.build_meta import build_wheel
backend_class = build_wheel.__self__.__class__
diff --git a/conftest.py b/conftest.py
index e123cca80..4dcd5e053 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1 +1,3 @@
+from __future__ import annotations
+
pytest_plugins = ["Tests.helper"]
diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh
index a318bfafd..04bfbc755 100755
--- a/depends/download-and-extract.sh
+++ b/depends/download-and-extract.sh
@@ -5,7 +5,7 @@ archive=$1
url=$2
if [ ! -f $archive.tar.gz ]; then
- wget -O $archive.tar.gz $url
+ wget --no-verbose -O $archive.tar.gz $url
fi
rmdir $archive
diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh
index b7cebbdbf..3adae91a5 100755
--- a/depends/install_imagequant.sh
+++ b/depends/install_imagequant.sh
@@ -1,15 +1,39 @@
#!/bin/bash
# install libimagequant
-archive=libimagequant-4.2.2
+archive_name=libimagequant
+archive_version=4.2.2
-./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
+archive=$archive_name-$archive_version
-pushd $archive/imagequant-sys
+if [[ "$GHA_LIBIMAGEQUANT_CACHE_HIT" == "true" ]]; then
-cargo install cargo-c
-cargo cinstall --prefix=/usr --destdir=.
-sudo cp usr/lib/libimagequant.so* /usr/lib/
-sudo cp usr/include/libimagequant.h /usr/include/
+ # Copy cached files into place
+ sudo cp ~/cache-$archive_name/libimagequant.so* /usr/lib/
+ sudo cp ~/cache-$archive_name/libimagequant.h /usr/include/
-popd
+else
+
+ # Build from source
+ ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
+
+ pushd $archive/imagequant-sys
+
+ cargo install cargo-c
+ cargo cinstall --prefix=/usr --destdir=.
+
+ # Copy into place
+ sudo cp usr/lib/libimagequant.so* /usr/lib/
+ sudo cp usr/include/libimagequant.h /usr/include/
+
+ if [ -n "$GITHUB_ACTIONS" ]; then
+ # Copy to cache
+ rm -rf ~/cache-$archive_name
+ mkdir ~/cache-$archive_name
+ cp usr/lib/libimagequant.so* ~/cache-$archive_name/
+ cp usr/include/libimagequant.h ~/cache-$archive_name/
+ fi
+
+ popd
+
+fi
diff --git a/docs/COPYING b/docs/COPYING
index bc44ba388..73af6d99c 100644
--- a/docs/COPYING
+++ b/docs/COPYING
@@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
- Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors
+ Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors
Like PIL, Pillow is licensed under the open source PIL
Software License:
diff --git a/docs/Guardfile b/docs/Guardfile
index 6cbf07b06..16a891a73 100755
--- a/docs/Guardfile
+++ b/docs/Guardfile
@@ -1,4 +1,6 @@
#!/usr/bin/env python3
+from __future__ import annotations
+
from livereload.compiler import shell
from livereload.task import Task
diff --git a/docs/PIL.rst b/docs/PIL.rst
index fa036b9cc..bdbf1373d 100644
--- a/docs/PIL.rst
+++ b/docs/PIL.rst
@@ -69,10 +69,10 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.ImageTransform` Module
----------------------------------
+:mod:`~PIL.ImageMode` Module
+----------------------------
-.. automodule:: PIL.ImageTransform
+.. automodule:: PIL.ImageMode
:members:
:undoc-members:
:show-inheritance:
diff --git a/docs/about.rst b/docs/about.rst
index 872ac0ea6..cdb32ca5d 100644
--- a/docs/about.rst
+++ b/docs/about.rst
@@ -6,15 +6,14 @@ Goals
The fork author's goal is to foster and support active development of PIL through:
-- Continuous integration testing via `GitHub Actions`_, `AppVeyor`_ and `Travis CI`_
+- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_
- Publicized development activity on `GitHub`_
- Regular releases to the `Python Package Index`_
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
-.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow
.. _GitHub: https://github.com/python-pillow/Pillow
-.. _Python Package Index: https://pypi.org/project/Pillow/
+.. _Python Package Index: https://pypi.org/project/pillow/
License
-------
diff --git a/docs/conf.py b/docs/conf.py
index 7dffcfae2..9ae7ae605 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -15,6 +15,7 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
+from __future__ import annotations
import PIL
@@ -53,7 +54,7 @@ master_doc = "index"
# General information about the project.
project = "Pillow (PIL Fork)"
copyright = (
- "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors"
+ "1995-2011 Fredrik Lundh, 2010-2024 Jeffrey A. Clark (Alex) and contributors"
)
author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors"
@@ -166,6 +167,12 @@ html_static_path = ["resources"]
# directly to the root of the documentation.
# html_extra_path = []
+html_css_files = ["css/dark.css"]
+
+html_js_files = [
+ "js/activate_tab.js",
+]
+
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
@@ -226,7 +233,7 @@ htmlhelp_basename = "PillowPILForkdoc"
# -- Options for LaTeX output ---------------------------------------------
-latex_elements = {
+latex_elements: dict[str, str] = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
@@ -313,19 +320,15 @@ texinfo_documents = [
# texinfo_no_detailmenu = False
-def setup(app):
- app.add_css_file("css/dark.css")
-
-
linkcheck_allowed_redirects = {
- r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*", # noqa: E501
- r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501
- r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501
- r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501
+ r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*",
+ r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg",
+ r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",
+ r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",
r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
- r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501
- r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501
- r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501
+ r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*",
+ r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",
+ r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",
}
# sphinx.ext.extlinks
@@ -338,6 +341,7 @@ extlinks = {
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
"issue": (_repo + "issues/%s", "#%s"),
"pr": (_repo + "pull/%s", "#%s"),
+ "pypi": ("https://pypi.org/project/%s/", "%s"),
}
# sphinxext.opengraph
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index ce956cade..205fcb9ab 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -10,7 +10,7 @@ Deprecated features
-------------------
Below are features which are considered deprecated. Where appropriate,
-a ``DeprecationWarning`` is issued.
+a :py:exc:`DeprecationWarning` is issued.
PSFile
~~~~~~
@@ -34,6 +34,64 @@ 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.
+ImageFile.raise_oserror
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 10.2.0
+
+``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow
+12.0.0 (2025-10-15). The function is undocumented and is only useful for translating
+error codes returned by a codec's ``decode()`` method, which ImageFile already does
+automatically.
+
+IptcImageFile helper functions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 10.2.0
+
+The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
+``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow
+12.0.0 (2025-10-15). These are undocumented helper functions intended
+for internal use, so there is no replacement. They can each be replaced
+by a single line of code using builtin functions in Python.
+
+ImageCms constants and versions() function
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 10.3.0
+
+A number of constants and a function in :py:mod:`.ImageCms` have been deprecated.
+This includes a table of flags based on LittleCMS version 1 which has been
+replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags.
+
+============================================ ====================================================
+Deprecated Use instead
+============================================ ====================================================
+``ImageCms.DESCRIPTION`` No replacement
+``ImageCms.VERSION`` ``PIL.__version__``
+``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION`
+``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT`
+``ImageCms.FLAGS["MATRIXONLY"]`` No replacement
+``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP`
+``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION`
+``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS`
+``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE`
+``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE`
+``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM`
+``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC`
+``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC`
+``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK`
+``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
+``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
+``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING`
+``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES`
+``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF`
+``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()`
+``ImageCms.versions()`` :py:func:`PIL.features.version_module` with
+ ``feature="littlecms2"``, :py:data:`sys.version` or
+ :py:data:`sys.version_info`, and ``PIL.__version__``
+============================================ ====================================================
+
Removed features
----------------
@@ -97,7 +155,7 @@ Constants
.. versionremoved:: 10.0.0
A number of constants have been removed.
-Instead, ``enum.IntEnum`` classes have been added.
+Instead, :py:class:`enum.IntEnum` classes have been added.
.. note::
@@ -267,7 +325,7 @@ ImageFile.raise_ioerror
.. deprecated:: 7.2.0
.. versionremoved:: 9.0.0
-``IOError`` was merged into ``OSError`` in Python 3.3.
+:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3.
So, ``ImageFile.raise_ioerror`` has been removed.
Use ``ImageFile.raise_oserror`` instead.
@@ -293,9 +351,9 @@ im.offset
``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead.
It was documented as deprecated in PIL 1.1.2,
-raised a ``DeprecationWarning`` since 1.1.5,
-an ``Exception`` since Pillow 3.0.0
-and ``NotImplementedError`` since 3.3.0.
+raised a :py:exc:`DeprecationWarning` since 1.1.5,
+an :py:exc:`Exception` since Pillow 3.0.0
+and :py:exc:`NotImplementedError` since 3.3.0.
Image.fromstring, im.fromstring and im.tostring
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -307,9 +365,9 @@ Image.fromstring, im.fromstring and im.tostring
* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead.
* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead.
-They issued a ``DeprecationWarning`` since 2.0.0,
-an ``Exception`` since 3.0.0
-and ``NotImplementedError`` since 3.3.0.
+They issued a :py:exc:`DeprecationWarning` since 2.0.0,
+an :py:exc:`Exception` since 3.0.0
+and :py:exc:`NotImplementedError` since 3.3.0.
ImageCms.CmsProfile attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -317,8 +375,8 @@ ImageCms.CmsProfile attributes
.. deprecated:: 3.2.0
.. versionremoved:: 8.0.0
-Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0,
-they issued a ``DeprecationWarning``:
+Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed.
+From 6.0.0, they issued a :py:exc:`DeprecationWarning`:
======================== ===================================================
Removed Use instead
@@ -442,7 +500,7 @@ PIL.OleFileIO
.. deprecated:: 4.0.0
.. versionremoved:: 6.0.0
-PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
-the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0
+``PIL.OleFileIO`` was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
+the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
PyPI (eg. ``python3 -m pip install olefile``).
diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py
index 61690410b..e98bb8680 100644
--- a/docs/example/DdsImagePlugin.py
+++ b/docs/example/DdsImagePlugin.py
@@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/
"""
+from __future__ import annotations
import struct
from io import BytesIO
diff --git a/docs/example/anchors.py b/docs/example/anchors.py
index 3447de4f7..b5d76b4fe 100644
--- a/docs/example/anchors.py
+++ b/docs/example/anchors.py
@@ -1,9 +1,11 @@
+from __future__ import annotations
+
from PIL import Image, ImageDraw, ImageFont
font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16)
-def test(anchor):
+def test(anchor: str) -> Image.Image:
im = Image.new("RGBA", (200, 100), "white")
d = ImageDraw.Draw(im)
d.line(((100, 0), (100, 100)), "gray")
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index d5d95d3ce..569ccb769 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -266,9 +266,13 @@ following options are available::
:py:class:`PIL.ImagePalette.ImagePalette` object.
**optimize**
- If present and true, attempt to compress the palette by
- eliminating unused colors. This is only useful if the palette can
- be compressed to the next smaller power of 2 elements.
+ Whether to attempt to compress the palette by eliminating unused colors
+ (this is only useful if the palette can be compressed to the next smaller
+ power of 2 elements) and whether to mark all pixels that are not new in the
+ next frame as transparent.
+
+ This is attempted by default, unless a palette is specified as an option or
+ as part of the first image's :py:attr:`~PIL.Image.Image.info` dictionary.
Note that if the image you are saving comes from an existing GIF, it may have
the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary.
@@ -483,6 +487,16 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**exif**
If present, the image will be stored with the provided raw EXIF data.
+**keep_rgb**
+ By default, libjpeg converts images with an RGB color space to YCbCr.
+ If this option is present and true, those images will be stored as RGB
+ instead.
+
+ When this option is enabled, attempting to chroma-subsample RGB images
+ with the ``subsampling`` option will raise an :py:exc:`OSError`.
+
+ .. versionadded:: 10.2.0
+
**subsampling**
If present, sets the subsampling for the encoder.
@@ -494,6 +508,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
If absent, the setting will be determined by libjpeg or libjpeg-turbo.
+**restart_marker_blocks**
+ If present, emit a restart marker whenever the specified number of MCU
+ blocks has been produced.
+
+ .. versionadded:: 10.2.0
+
+**restart_marker_rows**
+ If present, emit a restart marker whenever the specified number of MCU
+ rows has been produced.
+
+ .. versionadded:: 10.2.0
+
**qtables**
If present, sets the qtables for the encoder. This is listed as an
advanced option for wizards in the JPEG documentation. Use with
@@ -506,6 +532,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
.. versionadded:: 2.5.0
+**streamtype**
+ Allows storing images without quantization and Huffman tables, or with
+ these tables but without image data. This is useful for container formats
+ or network protocols that handle tables separately and share them between
+ images.
+
+ * ``0`` (default): interchange datastream, with tables and image data
+ * ``1``: abbreviated table specification (tables-only) datastream
+
+ .. versionadded:: 10.2.0
+
+ * ``2``: abbreviated image (image-only) datastream
+
**comment**
A comment about the image.
@@ -523,12 +562,13 @@ JPEG 2000
.. versionadded:: 2.4.0
-Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB`` or
-``RGBA`` data. It can also read files containing ``YCbCr`` data, which it
-converts on read into ``RGB`` or ``RGBA`` depending on whether or not there is
-an alpha channel. Pillow supports JPEG 2000 raw codestreams (``.j2k`` files),
-as well as boxed JPEG 2000 files (``.j2p`` or ``.jpx`` files). Pillow does
-*not* support files whose components have different sampling frequencies.
+Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
+``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
+``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel.
+Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``,
+``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports
+JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files
+(``.jp2`` or ``.jpx`` files).
When loading, if you set the ``mode`` on the image prior to the
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to
@@ -656,6 +696,25 @@ PCX
Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data.
+PFM
+^^^
+
+.. versionadded:: 10.3.0
+
+Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files
+containing ``F`` data.
+
+Color (PF format) PFM files are not supported.
+
+Opening
+~~~~~~~
+
+The :py:func:`~PIL.Image.open` function sets the following
+:py:attr:`~PIL.Image.Image.info` properties:
+
+**scale**
+ The absolute value of the number stored in the *Scale Factor / Endianness* line.
+
PNG
^^^
@@ -1296,6 +1355,8 @@ Pillow reads Kodak FlashPix files. In the current version, only the highest
resolution image is read from the file, and the viewing transform is not taken
into account.
+To enable FPX support, you must install :pypi:`olefile`.
+
.. note::
To enable full FlashPix support, you need to build and install the IJG JPEG
@@ -1372,6 +1433,8 @@ the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.s
Note that there may be an embedded gamma of 2.2 in MIC files.
+To enable MIC support, you must install :pypi:`olefile`.
+
MPO
^^^
diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst
index 2fbce86d4..523e2ad74 100644
--- a/docs/handbook/tutorial.rst
+++ b/docs/handbook/tutorial.rst
@@ -26,7 +26,7 @@ image. If the image was not read from a file, it is set to None. The size
attribute is a 2-tuple containing width and height (in pixels). The
:py:attr:`~PIL.Image.Image.mode` attribute defines the number and names of the
bands in the image, and also the pixel type and depth. Common modes are “L”
-(luminance) for greyscale images, “RGB” for true color images, and “CMYK” for
+(luminance) for grayscale images, “RGB” for true color images, and “CMYK” for
pre-press images.
If the file cannot be opened, an :py:exc:`OSError` exception is raised.
@@ -542,7 +542,7 @@ Reading from URL
from PIL import Image
from urllib.request import urlopen
- url = "https://python-pillow.org/images/pillow-logo.png"
+ url = "https://python-pillow.org/assets/images/pillow-logo.png"
img = Image.open(urlopen(url))
@@ -599,7 +599,7 @@ Controlling the decoder
Some decoders allow you to manipulate the image while reading it from a file.
This can often be used to speed up decoding when creating thumbnails (when
speed is usually more important than quality) and printing to a monochrome
-laser printer (when only a greyscale version of the image is needed).
+laser printer (when only a grayscale version of the image is needed).
The :py:meth:`~PIL.Image.Image.draft` method manipulates an opened but not yet
loaded image so it as closely as possible matches the given mode and size. This
diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst
index ad1bf7f95..956d63aa7 100644
--- a/docs/handbook/writing-your-own-image-plugin.rst
+++ b/docs/handbook/writing-your-own-image-plugin.rst
@@ -45,7 +45,7 @@ Example
The following plugin supports a simple format, which has a 128-byte header
consisting of the words “SPAM” followed by the width, height, and pixel size in
bits. The header fields are separated by spaces. The image data follows
-directly after the header, and can be either bi-level, greyscale, or 24-bit
+directly after the header, and can be either bi-level, grayscale, or 24-bit
true color.
**SpamImagePlugin.py**::
@@ -211,9 +211,9 @@ table describes some commonly used **raw modes**:
| ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the |
| | | least significant bit. 0 means black, 1 means white. |
+-----------+-------------------------------------------------------------------+
-| ``L`` | 8-bit greyscale. 0 means black, 255 means white. |
+| ``L`` | 8-bit grayscale. 0 means black, 255 means white. |
+-----------+-------------------------------------------------------------------+
-| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. |
+| ``L;I`` | 8-bit inverted grayscale. 0 means white, 255 means black. |
+-----------+-------------------------------------------------------------------+
| ``P`` | 8-bit palette-mapped image. |
+-----------+-------------------------------------------------------------------+
diff --git a/docs/index.rst b/docs/index.rst
index 4f577fe9c..558369919 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more
+ document.addEventListener('DOMContentLoaded', function() {
+ activateTab(getOS());
+ });
+
+
Warnings
--------
@@ -42,6 +50,11 @@ Install Pillow with :command:`pip`::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
+Optionally, install :pypi:`defusedxml` for Pillow to read XMP data,
+and :pypi:`olefile` for Pillow to read FPX and MIC images::
+
+ python3 -m pip install --upgrade defusedxml olefile
+
.. tab:: Linux
@@ -82,11 +95,10 @@ Install Pillow with :command:`pip`::
.. tab:: Windows
- .. warning:: Pillow > 9.5.0 no longer includes 32-bit wheels.
-
- We provide Pillow binaries for Windows compiled for the matrix of
- supported Pythons in 64-bit versions in the wheel format. These binaries include
- support for all optional libraries except libimagequant and libxcb. Raqm support
+ We provide Pillow binaries for Windows compiled for the matrix of supported
+ Pythons in the wheel format. These include x86, x86-64 and arm64 versions
+ (with the exception of Python 3.8 on arm64). These binaries include support
+ for all optional libraries except libimagequant and libxcb. Raqm support
requires FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
@@ -146,13 +158,13 @@ Many of Pillow's features require external libraries:
* Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and
libjpeg-turbo version **8**.
- * Starting with Pillow 3.0.0, libjpeg is required by default, but
- may be disabled with the ``--disable-jpeg`` flag.
+ * Starting with Pillow 3.0.0, libjpeg is required by default. It can be
+ disabled with the ``-C jpeg=disable`` flag.
* **zlib** provides access to compressed PNGs
- * Starting with Pillow 3.0.0, zlib is required by default, but may
- be disabled with the ``--disable-zlib`` flag.
+ * Starting with Pillow 3.0.0, zlib is required by default. It can be
+ disabled with the ``-C zlib=disable`` flag.
* **libtiff** provides compressed TIFF functionality
@@ -163,7 +175,7 @@ Many of Pillow's features require external libraries:
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
- above uses liblcms2. Tested with **1.19** and **2.7-2.15**.
+ above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
* **libwebp** provides the WebP format.
@@ -171,8 +183,6 @@ Many of Pillow's features require external libraries:
transparent WebP files. Versions **0.3.0** and above support
transparency.
-* **tcl/tk** provides support for tkinter bitmap and photo images.
-
* **openjpeg** provides JPEG 2000 functionality.
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
@@ -356,7 +366,7 @@ for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
additional configuration should be required. If they are installed in
a non-standard location, you may need to configure setuptools to use
those locations by editing :file:`setup.py` or
-:file:`setup.cfg`, or by adding environment variables on the command
+:file:`pyproject.toml`, or by adding environment variables on the command
line::
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
@@ -375,7 +385,7 @@ After navigating to the Pillow directory, run::
python3 -m pip install --upgrade pip
python3 -m pip install .
-.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files
+.. _compressed archive from PyPI: https://pypi.org/project/pillow/#files
Build Options
"""""""""""""
@@ -456,10 +466,10 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
-| Fedora 37 | 3.11 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
+| Fedora 39 | 3.12 | x86-64 |
++----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
@@ -478,7 +488,7 @@ These platforms are built and tested for every change.
| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, PyPy3 | |
| +----------------------------+---------------------+
-| | 3.11 | x86 |
+| | 3.12 | x86 |
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+
@@ -500,7 +510,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+
-| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm |
+| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm |
+----------------------------------+----------------------------+------------------+--------------+
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
| +----------------------------+------------------+ |
@@ -571,6 +581,10 @@ These platforms have been reported to work at the versions mentioned.
+----------------------------------+----------------------------+------------------+--------------+
| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
+----------------------------------+----------------------------+------------------+--------------+
+| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm64 |
++----------------------------------+----------------------------+------------------+--------------+
+| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 |
++----------------------------------+----------------------------+------------------+--------------+
| Windows 10 | 3.7 | 7.1.0 |x86-64 |
+----------------------------------+----------------------------+------------------+--------------+
| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
@@ -588,5 +602,5 @@ Old Versions
------------
You can download old distributions from the `release history at PyPI
-`_ and by direct URL access
-eg. https://pypi.org/project/Pillow/1.0/.
+`_ and by direct URL access
+eg. https://pypi.org/project/pillow/1.0/.
diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst
index 464ab77ea..06965ead3 100644
--- a/docs/reference/ExifTags.rst
+++ b/docs/reference/ExifTags.rst
@@ -4,8 +4,9 @@
:py:mod:`~PIL.ExifTags` Module
==============================
-The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes
-which provide constants and clear-text names for various well-known EXIF tags.
+The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum`
+classes which provide constants and clear-text names for various well-known
+EXIF tags.
.. py:data:: Base
diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst
index 9b9b5e7b2..c4484cbe2 100644
--- a/docs/reference/ImageCms.rst
+++ b/docs/reference/ImageCms.rst
@@ -8,9 +8,34 @@ The :py:mod:`~PIL.ImageCms` module provides color profile management
support using the LittleCMS2 color management engine, based on Kevin
Cazabon's PyCMS library.
+.. autoclass:: ImageCmsProfile
+ :members:
+ :special-members: __init__
.. autoclass:: ImageCmsTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
.. autoexception:: PyCMSError
+Constants
+---------
+
+.. autoclass:: Intent
+ :members:
+ :member-order: bysource
+ :undoc-members:
+ :show-inheritance:
+.. autoclass:: Direction
+ :members:
+ :member-order: bysource
+ :undoc-members:
+ :show-inheritance:
+.. autoclass:: Flags
+ :members:
+ :member-order: bysource
+ :undoc-members:
+ :show-inheritance:
+
Functions
---------
@@ -37,13 +62,15 @@ CmsProfile
----------
The ICC color profiles are wrapped in an instance of the class
-:py:class:`CmsProfile`. The specification ICC.1:2010 contains more
+:py:class:`~core.CmsProfile`. The specification ICC.1:2010 contains more
information about the meaning of the values in ICC profiles.
For convenience, all XYZ-values are also given as xyY-values (so they
can be easily displayed in a chromaticity diagram, for example).
+.. py:currentmodule:: PIL.ImageCms.core
.. py:class:: CmsProfile
+ :canonical: PIL._imagingcms.CmsProfile
.. py:attribute:: creation_date
:type: Optional[datetime.datetime]
diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst
index 20237eccf..31faeac78 100644
--- a/docs/reference/ImageColor.rst
+++ b/docs/reference/ImageColor.rst
@@ -59,7 +59,7 @@ Functions
.. py:method:: getcolor(color, mode)
Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
- greyscale value if the mode is not color or a palette image. If the string
+ grayscale value if the mode is not color or a palette image. If the string
cannot be parsed, this function raises a :py:exc:`ValueError` exception.
.. versionadded:: 1.1.4
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index d5a093ac0..4ccfacae7 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -362,9 +362,10 @@ Methods
:param fill: Color to use for the text.
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values. This parameter is
- ignored for non-TrueType fonts.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+ This parameter is ignored for non-TrueType fonts.
.. note:: This parameter was present in earlier versions
of Pillow, but implemented only in version 8.0.0.
@@ -433,9 +434,10 @@ Methods
:param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values. This parameter is
- ignored for non-TrueType fonts.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+ This parameter is ignored for non-TrueType fonts.
.. note:: This parameter was present in earlier versions
of Pillow, but implemented only in version 8.0.0.
@@ -576,9 +578,10 @@ Methods
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`.
:param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values. This parameter is
- ignored for non-TrueType fonts.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+ This parameter is ignored for non-TrueType fonts.
:param spacing: If the text is passed on to
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
the number of pixels between lines.
@@ -630,9 +633,10 @@ Methods
:param text: Text to be measured.
:param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values. This parameter is
- ignored for non-TrueType fonts.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+ This parameter is ignored for non-TrueType fonts.
:param spacing: The number of pixels between lines.
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst
index 457f0d4df..529acad4a 100644
--- a/docs/reference/ImageEnhance.rst
+++ b/docs/reference/ImageEnhance.rst
@@ -58,7 +58,7 @@ method:
This class can be used to control the contrast of an image, similar to the
contrast control on a TV set. An
- :ref:`enhancement factor ` of 0.0 gives a solid grey
+ :ref:`enhancement factor ` of 0.0 gives a solid gray
image, a factor of 1.0 gives the original image, and greater values
increase the contrast of the image.
diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst
index 2abfa0cc9..6edf4b05c 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -10,7 +10,7 @@ this class store bitmap fonts, and are used with the
PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use
`pilfont.py `_
-from `pillow-scripts `_ to convert BDF and
+from :pypi:`pillow-scripts` to convert BDF and
PCF font descriptors (X window font formats) to this format.
Starting with version 1.1.4, PIL can be configured to support TrueType and
@@ -20,7 +20,7 @@ the imToolkit package.
.. warning::
To protect against potential DOS attacks when using arbitrary strings as
- text input, Pillow will raise a ``ValueError`` if the number of characters
+ text input, Pillow will raise a :py:exc:`ValueError` if the number of characters
is over a certain limit, :py:data:`MAX_STRING_LENGTH`.
This threshold can be changed by setting
@@ -70,24 +70,23 @@ Methods
Constants
---------
-.. data:: PIL.ImageFont.Layout.BASIC
+.. class:: Layout
- Use basic text layout for TrueType font.
- Advanced features such as text direction are not supported.
+ .. py:attribute:: BASIC
-.. data:: PIL.ImageFont.Layout.RAQM
+ Use basic text layout for TrueType font.
+ Advanced features such as text direction are not supported.
- Use Raqm text layout for TrueType font.
- Advanced features are supported.
+ .. py:attribute:: RAQM
- Requires Raqm, you can check support using
- :py:func:`PIL.features.check_feature` with ``feature="raqm"``.
+ Use Raqm text layout for TrueType font.
+ Advanced features are supported.
-Constants
----------
+ Requires Raqm, you can check support using
+ :py:func:`PIL.features.check_feature` with ``feature="raqm"``.
.. data:: MAX_STRING_LENGTH
Set to 1,000,000, to protect against potential DOS attacks. Pillow will
- raise a ``ValueError`` if the number of characters is over this limit. The
+ raise a :py:exc:`ValueError` if the number of characters is over this limit. The
check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst
index 0b94032d5..db2987eb0 100644
--- a/docs/reference/ImageGrab.rst
+++ b/docs/reference/ImageGrab.rst
@@ -11,9 +11,9 @@ or the clipboard to a PIL image memory.
.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=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.
+ 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
@@ -22,7 +22,10 @@ or the clipboard to a PIL image memory.
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux)
:param bbox: What region to copy. Default is the entire screen.
- Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used.
+ On macOS, this is not increased to 2x for Retina screens, so the full
+ width of a Retina screen would be 1440, not 2880.
+ On Windows, the top-left point may be negative if ``all_screens=True``
+ is used.
:param include_layered_windows: Includes layered windows. Windows OS only.
.. versionadded:: 6.1.0
diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst
index 118d988d6..ee07efa01 100644
--- a/docs/reference/ImageMath.rst
+++ b/docs/reference/ImageMath.rst
@@ -72,7 +72,7 @@ pixel bits.
Note that the operands are converted to 32-bit signed integers before the
bitwise operation is applied. This means that you’ll get negative values if
-you invert an ordinary greyscale image. You can use the and (&) operator to
+you invert an ordinary grayscale image. You can use the and (&) operator to
mask off unwanted bits.
Bitwise operators don’t work on floating point images.
diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst
new file mode 100644
index 000000000..5b0a5ce49
--- /dev/null
+++ b/docs/reference/ImageTransform.rst
@@ -0,0 +1,40 @@
+
+.. py:module:: PIL.ImageTransform
+.. py:currentmodule:: PIL.ImageTransform
+
+:py:mod:`~PIL.ImageTransform` Module
+====================================
+
+The :py:mod:`~PIL.ImageTransform` module contains implementations of
+:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin
+:py:class:`.Image.Transform` methods.
+
+.. autoclass:: Transform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: AffineTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: PerspectiveTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: ExtentTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: QuadTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: MeshTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/reference/index.rst b/docs/reference/index.rst
index 5d6affa94..82c75e373 100644
--- a/docs/reference/index.rst
+++ b/docs/reference/index.rst
@@ -25,6 +25,7 @@ Reference
ImageShow
ImageStat
ImageTk
+ ImageTransform
ImageWin
ExifTags
TiffTags
diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst
index 363a67d9b..f2932c322 100644
--- a/docs/reference/internal_modules.rst
+++ b/docs/reference/internal_modules.rst
@@ -25,6 +25,19 @@ Internal Modules
:undoc-members:
:show-inheritance:
+:mod:`~PIL._typing` Module
+--------------------------
+
+.. module:: PIL._typing
+
+Provides a convenient way to import type hints that are not available
+on some Python versions.
+
+.. py:data:: TypeGuard
+ :value: typing.TypeGuard
+
+ See :py:obj:`typing.TypeGuard`.
+
:mod:`~PIL._util` Module
------------------------
diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst
index fcf4514a8..18cd99cf3 100644
--- a/docs/reference/plugins.rst
+++ b/docs/reference/plugins.rst
@@ -33,6 +33,14 @@ Plugin reference
:undoc-members:
:show-inheritance:
+:mod:`~PIL.DdsImagePlugin` Module
+---------------------------------
+
+.. automodule:: PIL.DdsImagePlugin
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
:mod:`~PIL.EpsImagePlugin` Module
---------------------------------
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index 06acfc7af..705ca0415 100644
--- a/docs/releasenotes/10.0.0.rst
+++ b/docs/releasenotes/10.0.0.rst
@@ -43,7 +43,7 @@ Constants
^^^^^^^^^
A number of constants have been removed.
-Instead, ``enum.IntEnum`` classes have been added.
+Instead, :py:class:`enum.IntEnum` classes have been added.
===================================================== ============================================================
Removed Use instead
@@ -173,8 +173,8 @@ been processed before Pillow started checking for decompression bombs.
Added ImageFont.MAX_STRING_LENGTH
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-To protect against potential DOS attacks when using arbitrary strings as text
-input, Pillow will now raise a ``ValueError`` if the number of characters
+:cve:`2023-44271`: To protect against potential DOS attacks when using arbitrary strings as text
+input, Pillow will now raise a :py:exc:`ValueError` if the number of characters
passed into ImageFont methods is over a certain limit,
:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.
diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst
index 8c3413c8c..fd556bdf1 100644
--- a/docs/releasenotes/10.1.0.rst
+++ b/docs/releasenotes/10.1.0.rst
@@ -8,7 +8,7 @@ Setting image mode
^^^^^^^^^^^^^^^^^^
If you attempt to set the mode of an image directly, e.g.
-``im.mode = "RGBA"``, you will now receive an ``AttributeError``. This is
+``im.mode = "RGBA"``, you will now receive an :py:exc:`AttributeError`. This is
not about removing existing functionality, but instead about raising an
explicit error to prevent later consequences. The ``convert`` method is the
correct way to change an image's mode.
diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst
new file mode 100644
index 000000000..c3947f64c
--- /dev/null
+++ b/docs/releasenotes/10.2.0.rst
@@ -0,0 +1,161 @@
+10.2.0
+------
+
+Deprecations
+============
+
+ImageFile.raise_oserror
+^^^^^^^^^^^^^^^^^^^^^^^
+
+``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow
+12.0.0 (2025-10-15). The function is undocumented and is only useful for translating
+error codes returned by a codec's ``decode()`` method, which ImageFile already does
+automatically.
+
+IptcImageFile helper functions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
+``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow
+12.0.0 (2025-10-15). These are undocumented helper functions intended
+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
+===========
+
+Zero or negative font size error
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When creating a :py:class:`~PIL.ImageFont.FreeTypeFont` instance, either directly or
+through :py:func:`~PIL.ImageFont.truetype`, if the font size is zero or less, a
+:py:exc:`ValueError` will now be raised.
+
+API Additions
+=============
+
+Added DdsImagePlugin enums
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:class:`~PIL.DdsImagePlugin.DDSD`, :py:class:`~PIL.DdsImagePlugin.DDSCAPS`,
+:py:class:`~PIL.DdsImagePlugin.DDSCAPS2`, :py:class:`~PIL.DdsImagePlugin.DDPF`,
+:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT`
+enums have been added to :py:class:`PIL.DdsImagePlugin`.
+
+JPEG RGB color space
+^^^^^^^^^^^^^^^^^^^^
+
+When saving JPEG files, ``keep_rgb`` can now be set to ``True``. This will store RGB
+images in the RGB color space instead of being converted to YCbCr automatically by
+libjpeg. When this option is enabled, attempting to chroma-subsample RGB images with
+the ``subsampling`` option will raise an :py:exc:`OSError`.
+
+JPEG restart marker interval
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When saving JPEG files, ``restart_marker_blocks`` and ``restart_marker_rows`` can now
+be used to emit restart markers whenever the specified number of MCU blocks or rows
+have been produced.
+
+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.
+
+Security
+========
+
+ImageFont.getmask: Applied ImageFont.MAX_STRING_LENGTH
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To protect against potential DOS attacks when using arbitrary strings as text input,
+Pillow will now raise a :py:exc:`ValueError` if the number of characters passed into
+:py:meth:`PIL.ImageFont.ImageFont.getmask` is over a certain limit,
+:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.
+
+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``.
+
+A decompression bomb check has also been added to
+:py:meth:`PIL.ImageFont.ImageFont.getmask`.
+
+ImageFont.getmask: Trim glyph size
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To protect against potential DOS attacks when using PIL fonts,
+:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that
+they do not extend beyond the bitmap image.
+
+ImageMath.eval: Restricted environment keys
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:cve:`2023-50447`: If an attacker has control over the keys passed to the
+``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute
+arbitrary code. To prevent this, keys matching the names of builtins and keys
+containing double underscores will now raise a :py:exc:`ValueError`.
+
+Other Changes
+=============
+
+Added DDS BC4U and DX10 BC1 and BC4 reading
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added to read the BC4U format of DDS images.
+
+Support has also been added to read DX10 BC1 and BC4, whether UNORM or
+TYPELESS.
+
+Support arbitrary masks for uncompressed RGB DDS images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+All masks are now supported when reading DDS images with uncompressed RGB data,
+allowing for bit counts other than 24 and 32.
+
+Saving TIFF tag RowsPerStrip
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When saving TIFF images, the TIFF tag RowsPerStrip can now be one of the tags set by
+the user, rather than always being calculated by Pillow.
+
+Optimized ImageColor.getrgb and getcolor
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The color calculations of :py:attr:`~PIL.ImageColor.getrgb` and
+:py:attr:`~PIL.ImageColor.getcolor` are now cached using
+:py:func:`functools.lru_cache`. Cached calls of ``getrgb`` are 3.1 - 91.4 times
+as fast and ``getcolor`` are 5.1 - 19.6 times as fast.
+
+Optimized ImageMode.getmode
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using
+:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 1.2 times as
+fast.
+
+Optimized ImageStat.Stat count and extrema
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and
+:py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the
+histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on
+average and ``st.extrema`` is 12 times as fast on average.
+
+Encoder errors now report error detail as string
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:exc:`OSError` exceptions from image encoders now include a textual description of
+the error instead of a numeric error code.
+
+Type hints
+^^^^^^^^^^
+
+Work has begun to add type annotations to Pillow, including:
+
+* :py:mod:`~PIL.ContainerIO`
+* :py:mod:`~PIL.FontFile`, :py:mod:`~PIL.BdfFontFile` and :py:mod:`~PIL.PcfFontFile`
+* :py:mod:`~PIL.ImageChops`
+* :py:mod:`~PIL.ImageMode`
+* :py:mod:`~PIL.ImageSequence`
+* :py:mod:`~PIL.ImageTransform`
+* :py:mod:`~PIL.TarIO`
diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst
new file mode 100644
index 000000000..8772a382d
--- /dev/null
+++ b/docs/releasenotes/10.3.0.rst
@@ -0,0 +1,81 @@
+10.3.0
+------
+
+Backwards Incompatible Changes
+==============================
+
+TODO
+^^^^
+
+Deprecations
+============
+
+ImageCms constants and versions() function
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A number of constants and a function in :py:mod:`.ImageCms` have been deprecated.
+This includes a table of flags based on LittleCMS version 1 which has been replaced
+with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags.
+
+============================================ ====================================================
+Deprecated Use instead
+============================================ ====================================================
+``ImageCms.DESCRIPTION`` No replacement
+``ImageCms.VERSION`` ``PIL.__version__``
+``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION`
+``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT`
+``ImageCms.FLAGS["MATRIXONLY"]`` No replacement
+``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP`
+``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION`
+``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS`
+``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE`
+``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE`
+``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM`
+``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC`
+``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC`
+``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK`
+``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
+``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
+``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING`
+``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES`
+``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF`
+``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()`
+``ImageCms.versions()`` :py:func:`PIL.features.version_module` with
+ ``feature="littlecms2"``, :py:data:`sys.version` or
+ :py:data:`sys.version_info`, and ``PIL.__version__``
+============================================ ====================================================
+
+API Changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API Additions
+=============
+
+Added PerspectiveTransform
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning
+that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding
+subclass of :py:class:`~PIL.ImageTransform.Transform`.
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+Other Changes
+=============
+
+Portable FloatMap (PFM) images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added for reading and writing grayscale (Pf format)
+Portable FloatMap (PFM) files containing ``F`` data.
diff --git a/docs/releasenotes/2.8.0.rst b/docs/releasenotes/2.8.0.rst
index c522fe8b0..4dbbc0bdd 100644
--- a/docs/releasenotes/2.8.0.rst
+++ b/docs/releasenotes/2.8.0.rst
@@ -10,7 +10,7 @@ operations. As a result PIL was unable to open them as images, requiring a wrap
``cStringIO`` or ``BytesIO``.
Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and
-catch on exception ``AttributeError`` or ``io.UnsupportedOperation``. If this is caught we
+catch on exception :py:exc:`AttributeError` or :py:exc:`io.UnsupportedOperation`. If this is caught we
attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like
objects).
diff --git a/docs/releasenotes/3.4.0.rst b/docs/releasenotes/3.4.0.rst
index dc5e2e295..2bbafe741 100644
--- a/docs/releasenotes/3.4.0.rst
+++ b/docs/releasenotes/3.4.0.rst
@@ -19,7 +19,7 @@ Deprecation Warning when Saving JPEGs
JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0
silently drops the alpha channel. With this release Pillow will now
-issue a ``DeprecationWarning`` when attempting to save a ``RGBA`` mode
+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.
New DDS Decoders
diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst
index cbf131c93..5778de26a 100644
--- a/docs/releasenotes/4.0.0.rst
+++ b/docs/releasenotes/4.0.0.rst
@@ -17,8 +17,8 @@ Pillow 4.0 supports Python 3.6.
OleFileIO.py
============
-OleFileIO.py has been removed as a vendored file and is now installed
-from the upstream olefile pypi package. All internal dependencies are
+``OleFileIO.py`` has been removed as a vendored file and is now installed
+from the upstream :pypi:`olefile` PyPI package. All internal dependencies are
redirected to the olefile package. Direct accesses to
``PIL.OlefileIO`` raises a deprecation warning, then patches the
upstream olefile into ``sys.modules`` in its place.
diff --git a/docs/releasenotes/5.0.0.rst b/docs/releasenotes/5.0.0.rst
index 509edbe6d..be00a45cd 100644
--- a/docs/releasenotes/5.0.0.rst
+++ b/docs/releasenotes/5.0.0.rst
@@ -28,7 +28,7 @@ Scripts
The scripts formerly installed by Pillow have been split into a
separate package, pillow-scripts, living at
-https://github.com/python-pillow/pillow-scripts .
+https://github.com/python-pillow/pillow-scripts.
API Changes
@@ -37,7 +37,7 @@ API Changes
OleFileIO.py
^^^^^^^^^^^^
-The olefile module is no longer a required dependency when installing Pillow.
+The :pypi:`olefile` module is no longer a required dependency when installing Pillow.
Support for plugins requiring olefile will not be loaded if it is not
installed. This allows library consumers to avoid installing this dependency
if they choose. Some library consumers have little interest in the format
diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst
index bff56566b..8f276da24 100644
--- a/docs/releasenotes/5.3.0.rst
+++ b/docs/releasenotes/5.3.0.rst
@@ -8,7 +8,7 @@ Image size
^^^^^^^^^^
If you attempt to set the size of an image directly, e.g.
-``im.size = (100, 100)``, you will now receive an ``AttributeError``. This is
+``im.size = (100, 100)``, you will now receive an :py:exc:`AttributeError`. This is
not about removing existing functionality, but instead about raising an
explicit error to prevent later consequences. The ``resize`` method is the
correct way to change an image's size.
@@ -16,7 +16,8 @@ correct way to change an image's size.
The exceptions to this are:
* The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage.
-* The TIFF image format, which now has a ``DeprecationWarning`` for this action, as direct image size setting was previously necessary to work around an issue with tile extents.
+* The TIFF image format, which now has a :py:exc:`DeprecationWarning` for this action,
+ as direct image size setting was previously necessary to work around an issue with tile extents.
API Additions
diff --git a/docs/releasenotes/5.4.1.rst b/docs/releasenotes/5.4.1.rst
index 78f483db6..bbabd6520 100644
--- a/docs/releasenotes/5.4.1.rst
+++ b/docs/releasenotes/5.4.1.rst
@@ -15,7 +15,7 @@ PNG: Handle IDAT chunks after image end
Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop
reading image data before the IDAT chunks finish. A regression caused an
-``EOFError`` exception when previously there was none. This is now fixed, and
+:py:exc:`EOFError` exception when previously there was none. This is now fixed, and
file reading continues in case there are subsequent text chunks.
PNG: MIME type
@@ -30,7 +30,7 @@ File closing
^^^^^^^^^^^^
A regression caused an unsupported image file to report a
-``ValueError: seek of closed file`` exception instead of an ``OSError``. This
+``ValueError: seek of closed file`` exception instead of an :py:exc:`OSError`. This
has been fixed by ensuring that image plugins only close their internal ``__fp``
if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own
file pointers.
diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst
index 3e3b945a0..5e69f0b6b 100644
--- a/docs/releasenotes/6.0.0.rst
+++ b/docs/releasenotes/6.0.0.rst
@@ -14,8 +14,8 @@ Pillow for Python 3.4 is 5.4.1.
Removed deprecated PIL.OleFileIO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of
-the upstream olefile Python package, and replaced with an ``ImportError``. The
+``PIL.OleFileIO`` was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of
+the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError`. The
deprecated file has now been removed from Pillow. If needed, install from PyPI (eg.
``python3 -m pip install olefile``).
@@ -103,7 +103,7 @@ ImageCms.CmsProfile attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From
-6.0.0, they issue a ``DeprecationWarning``:
+6.0.0, they issue a :py:exc:`DeprecationWarning`:
======================== ===============================
Deprecated Use instead
diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst
index 76e13b061..ce3edc5fa 100644
--- a/docs/releasenotes/6.1.0.rst
+++ b/docs/releasenotes/6.1.0.rst
@@ -29,10 +29,10 @@ API Additions
Image.entropy
^^^^^^^^^^^^^
Calculates and returns the entropy for the image. A bilevel image (mode "1") is treated
-as a greyscale ("L") image by this method. If a mask is provided, the method employs
+as a grayscale ("L") image by this method. If a mask is provided, the method employs
the histogram for those parts of the image where the mask image is non-zero. The mask
image must have the same size as the image, and be either a bi-level image (mode "1") or
-a greyscale image ("L").
+a grayscale image ("L").
ImageGrab.grab
^^^^^^^^^^^^^^
@@ -58,7 +58,7 @@ file. ``ImageFont.FreeTypeFont`` has four new methods,
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and
:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes
-instead. An ``IOError`` will be raised if the font is not a variation font. FreeType
+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
diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst
index f2e235289..ed6026593 100644
--- a/docs/releasenotes/7.0.0.rst
+++ b/docs/releasenotes/7.0.0.rst
@@ -85,7 +85,7 @@ Custom unidentified image error
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be
-identified. For backwards compatibility, this will inherit from ``OSError``.
+identified. For backwards compatibility, this will inherit from :py:exc:`OSError`.
New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/docs/releasenotes/7.1.2.rst b/docs/releasenotes/7.1.2.rst
index b12d84e33..ec0063e79 100644
--- a/docs/releasenotes/7.1.2.rst
+++ b/docs/releasenotes/7.1.2.rst
@@ -7,7 +7,7 @@ Fix another regression seeking PNG files
This fixes a regression introduced in 7.1.0 when adding support for APNG files.
When calling ``seek(n)`` on a regular PNG where ``n > 0``, it failed to raise an
-``EOFError`` as it should have done, resulting in:
+:py:exc:`EOFError` as it should have done, resulting in:
.. code-block:: pycon
diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst
index ff1b7c9e7..91e54da19 100644
--- a/docs/releasenotes/7.2.0.rst
+++ b/docs/releasenotes/7.2.0.rst
@@ -53,6 +53,6 @@ a custom :py:class:`~PIL.ImageShow.Viewer` class.
ImageFile.raise_ioerror
~~~~~~~~~~~~~~~~~~~~~~~
-``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror``
+:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror``
is now deprecated and will be removed in a future release. Use
``ImageFile.raise_oserror`` instead.
diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst
index 00c691a74..1fc245c9a 100644
--- a/docs/releasenotes/8.0.0.rst
+++ b/docs/releasenotes/8.0.0.rst
@@ -30,7 +30,7 @@ Image.fromstring, im.fromstring and im.tostring
ImageCms.CmsProfile attributes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed:
+Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed:
======================== ===================================================
Removed Use instead
@@ -168,7 +168,7 @@ offset.
Error for large BMP files
^^^^^^^^^^^^^^^^^^^^^^^^^
-Previously, if a BMP file was too large, an ``OSError`` would be raised. Now,
+Previously, if a BMP file was too large, an :py:exc:`OSError` would be raised. Now,
``DecompressionBombError`` is used instead, as Pillow already uses for other formats.
Dark theme for docs
diff --git a/docs/releasenotes/8.3.1.rst b/docs/releasenotes/8.3.1.rst
index e97070c11..6af2b37bf 100644
--- a/docs/releasenotes/8.3.1.rst
+++ b/docs/releasenotes/8.3.1.rst
@@ -22,9 +22,10 @@ Catch OSError when checking if destination is sys.stdout
========================================================
In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was
-updated. This lead to an OSError being raised if the environment restricted access.
+updated. This lead to an :py:exc:`OSError` being raised if the environment restricted
+access.
-The OSError is now silently caught.
+The :py:exc:`OSError` is now silently caught.
Fixed removing orientation in ImageOps.exif_transpose
=====================================================
@@ -34,7 +35,7 @@ original image EXIF data was not modified, and the orientation was only removed
the modified copy.
However, for certain images the orientation was already missing from the modified
-image, leading to a KeyError.
+image, leading to a :py:exc:`KeyError`.
This error has been resolved, and the copying of metadata to the modified image
improved.
diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst
index 73e77ad3e..090ec8024 100644
--- a/docs/releasenotes/9.0.0.rst
+++ b/docs/releasenotes/9.0.0.rst
@@ -63,7 +63,7 @@ a custom :py:class:`~PIL.ImageShow.Viewer` class.
ImageFile.raise_ioerror
^^^^^^^^^^^^^^^^^^^^^^^
-``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror``
+:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror``
has been removed. Use ``ImageFile.raise_oserror`` instead.
diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst
index 19690ca59..6400218f4 100644
--- a/docs/releasenotes/9.1.0.rst
+++ b/docs/releasenotes/9.1.0.rst
@@ -8,14 +8,14 @@ Raise an error when performing a negative crop
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now
-it will raise a ``ValueError``, to help reduce confusion if a user has unintentionally
+it will raise a :py:exc:`ValueError`, to help reduce confusion if a user has unintentionally
provided the wrong arguments.
Added specific error if path coordinate type is incorrect
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Rather than returning a ``SystemError``, passing the incorrect types of coordinates into
-a path will now raise a more specific ``ValueError``, with the message "incorrect
+Rather than returning a :py:exc:`SystemError`, passing the incorrect types of coordinates into
+a path will now raise a more specific :py:exc:`ValueError`, with the message "incorrect
coordinate type".
Replace requirements.txt with extras
@@ -51,7 +51,7 @@ Constants
^^^^^^^^^
A number of constants have been deprecated and will be removed in Pillow 10.0.0
-(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
+(2023-07-01). Instead, :py:class:`enum.IntEnum` classes have been added.
.. note::
diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst
index fde2faae3..16075ce95 100644
--- a/docs/releasenotes/9.3.0.rst
+++ b/docs/releasenotes/9.3.0.rst
@@ -33,8 +33,9 @@ Added ExifTags enums
^^^^^^^^^^^^^^^^^^^^
The data from :py:data:`~PIL.ExifTags.TAGS` and
-:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum``
-classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`.
+:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as
+:py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and
+:py:data:`~PIL.ExifTags.GPS`.
Security
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index 1b1c353fd..e86f8082b 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,8 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 10.3.0
+ 10.2.0
10.1.0
10.0.1
10.0.0
diff --git a/docs/resources/js/activate_tab.js b/docs/resources/js/activate_tab.js
new file mode 100644
index 000000000..92522b5ce
--- /dev/null
+++ b/docs/resources/js/activate_tab.js
@@ -0,0 +1,36 @@
+// Based on https://stackoverflow.com/a/38241481/724176
+function getOS() {
+ const userAgent = window.navigator.userAgent,
+ platform = window.navigator.userAgentData?.platform || window.navigator.platform,
+ macosPlatforms = ["macOS", "Macintosh", "MacIntel", "MacPPC", "Mac68K"],
+ windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
+
+ if (macosPlatforms.includes(platform)) {
+ return "macOS";
+ } else if (windowsPlatforms.includes(platform)) {
+ return "Windows";
+ } else if (/Android/.test(userAgent)) {
+ return "Android";
+ } else if (/Linux/.test(platform)) {
+ return "Linux";
+ }
+}
+
+function activateTab(tabName) {
+ // Find all label elements with the specified tab name
+ const labels = document.querySelectorAll(".tab-label");
+
+ labels.forEach((label) => {
+ if (label.textContent == tabName) {
+ // Find the associated input element using the "for" attribute
+ const tabInputId = label.getAttribute("for");
+ const tabInput = document.getElementById(tabInputId);
+
+ // Check if the input element exists before attempting to set the "checked" attribute
+ if (tabInput) {
+ // Activate the tab by setting its "checked" attribute to true
+ tabInput.checked = true;
+ }
+ }
+ });
+}
diff --git a/pyproject.toml b/pyproject.toml
index fd9c05f92..b1ce9cf1d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,3 +6,151 @@ requires = [
backend-path = [
"_custom_build",
]
+
+[project]
+name = "pillow"
+description = "Python Imaging Library (Fork)"
+readme = "README.md"
+keywords = [
+ "Imaging",
+]
+license = {text = "HPND"}
+authors = [{name = "Jeffrey A. Clark (Alex)", email = "aclark@aclark.net"}]
+requires-python = ">=3.8"
+classifiers = [
+ "Development Status :: 6 - Mature",
+ "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Topic :: Multimedia :: Graphics",
+ "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
+ "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
+ "Topic :: Multimedia :: Graphics :: Graphics Conversion",
+ "Topic :: Multimedia :: Graphics :: Viewers",
+]
+dynamic = [
+ "version",
+]
+[project.optional-dependencies]
+docs = [
+ "furo",
+ "olefile",
+ "sphinx>=2.4",
+ "sphinx-copybutton",
+ "sphinx-inline-tabs",
+ "sphinx-removed-in",
+ "sphinxext-opengraph",
+]
+fpx = [
+ "olefile",
+]
+mic = [
+ "olefile",
+]
+tests = [
+ "check-manifest",
+ "coverage",
+ "defusedxml",
+ "markdown2",
+ "olefile",
+ "packaging",
+ "pyroma",
+ "pytest",
+ "pytest-cov",
+ "pytest-timeout",
+]
+typing = [
+ 'typing-extensions; python_version < "3.10"',
+]
+xmp = [
+ "defusedxml",
+]
+[project.urls]
+Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst"
+Documentation = "https://pillow.readthedocs.io"
+Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi"
+Homepage = "https://python-pillow.org"
+Mastodon = "https://fosstodon.org/@pillow"
+"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
+Source = "https://github.com/python-pillow/Pillow"
+Twitter = "https://twitter.com/PythonPillow"
+
+[tool.setuptools]
+packages = ["PIL"]
+include-package-data = true
+package-dir = {"" = "src"}
+
+[tool.setuptools.dynamic]
+version = {attr = "PIL.__version__"}
+
+[tool.cibuildwheel]
+before-all = ".github/workflows/wheels-dependencies.sh"
+build-verbosity = 1
+config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
+test-command = "cd {project} && .github/workflows/wheels-test.sh"
+test-extras = "tests"
+
+[tool.ruff]
+select = [
+ "C4", # flake8-comprehensions
+ "E", # pycodestyle errors
+ "EM", # flake8-errmsg
+ "F", # pyflakes errors
+ "I", # isort
+ "ISC", # flake8-implicit-str-concat
+ "PGH", # pygrep-hooks
+ "RUF100", # unused noqa (yesqa)
+ "UP", # pyupgrade
+ "W", # pycodestyle warnings
+ "YTT", # flake8-2020
+ # "LOG", # TODO: enable flake8-logging when it's not in preview anymore
+]
+extend-ignore = [
+ "E203", # Whitespace before ':'
+ "E221", # Multiple spaces before operator
+ "E226", # Missing whitespace around arithmetic operator
+ "E241", # Multiple spaces after ','
+]
+
+[tool.ruff.per-file-ignores]
+"Tests/oss-fuzz/fuzz_font.py" = ["I002"]
+"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"]
+
+[tool.ruff.isort]
+known-first-party = ["PIL"]
+required-imports = ["from __future__ import annotations"]
+
+[tool.pytest.ini_options]
+addopts = "-ra --color=yes"
+testpaths = ["Tests"]
+
+[tool.mypy]
+python_version = "3.8"
+pretty = true
+disallow_any_generics = true
+enable_error_code = "ignore-without-code"
+extra_checks = true
+follow_imports = "silent"
+warn_redundant_casts = true
+warn_unreachable = true
+warn_unused_ignores = true
+exclude = [
+ '^src/PIL/_tkinter_finder.py$',
+ '^src/PIL/DdsImagePlugin.py$',
+ '^src/PIL/FpxImagePlugin.py$',
+ '^src/PIL/Image.py$',
+ '^src/PIL/ImageQt.py$',
+ '^src/PIL/ImImagePlugin.py$',
+ '^src/PIL/MicImagePlugin.py$',
+ '^src/PIL/PdfParser.py$',
+ '^src/PIL/PyAccess.py$',
+ '^src/PIL/TiffImagePlugin.py$',
+ '^src/PIL/TiffTags.py$',
+ '^src/PIL/WebPImagePlugin.py$',
+]
diff --git a/selftest.py b/selftest.py
index 6eeadd1db..ed5252c44 100755
--- a/selftest.py
+++ b/selftest.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
# minimal sanity check
+from __future__ import annotations
import sys
@@ -14,7 +15,7 @@ except AttributeError:
pass
-def testimage():
+def testimage() -> None:
"""
PIL lets you create in-memory images with various pixel types:
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index e560f9516..000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,74 +0,0 @@
-[metadata]
-name = Pillow
-description = Python Imaging Library (Fork)
-long_description = file: README.md
-long_description_content_type = text/markdown
-url = https://python-pillow.org
-author = Jeffrey A. Clark (Alex)
-author_email = aclark@aclark.net
-license = HPND
-classifiers =
- Development Status :: 6 - Mature
- License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)
- Programming Language :: Python :: 3
- Programming Language :: Python :: 3 :: Only
- Programming Language :: Python :: 3.8
- Programming Language :: Python :: 3.9
- Programming Language :: Python :: 3.10
- Programming Language :: Python :: 3.11
- Programming Language :: Python :: 3.12
- Programming Language :: Python :: Implementation :: CPython
- Programming Language :: Python :: Implementation :: PyPy
- Topic :: Multimedia :: Graphics
- Topic :: Multimedia :: Graphics :: Capture :: Digital Camera
- Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
- Topic :: Multimedia :: Graphics :: Graphics Conversion
- Topic :: Multimedia :: Graphics :: Viewers
-keywords = Imaging
-project_urls =
- Documentation=https://pillow.readthedocs.io
- Source=https://github.com/python-pillow/Pillow
- Funding=https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi
- Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html
- Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
- Twitter=https://twitter.com/PythonPillow
- Mastodon=https://fosstodon.org/@pillow
-
-[options]
-packages = PIL
-python_requires = >=3.8
-include_package_data = True
-package_dir =
- = src
-
-[options.extras_require]
-docs =
- furo
- olefile
- sphinx>=2.4
- sphinx-copybutton
- sphinx-inline-tabs
- sphinx-removed-in
- sphinxext-opengraph
-tests =
- check-manifest
- coverage
- defusedxml
- markdown2
- olefile
- packaging
- pyroma
- pytest
- pytest-cov
- pytest-timeout
-
-[flake8]
-extend-ignore = E203
-max-line-length = 88
-
-[isort]
-profile = black
-
-[tool:pytest]
-addopts = -ra --color=yes
-testpaths = Tests
diff --git a/setup.py b/setup.py
old mode 100755
new mode 100644
index 935166716..1bbd2c05c
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python3
# > pyroma .
# ------------------------------
# Checking .
@@ -7,6 +6,7 @@
# Final rating: 10/10
# Your cheese is so fresh most people think it's a cream: Mascarpone
# ------------------------------
+from __future__ import annotations
import os
import re
@@ -440,17 +440,17 @@ class pil_build_ext(build_ext):
#
# add configured kits
- for root_name, lib_name in dict(
- JPEG_ROOT="libjpeg",
- JPEG2K_ROOT="libopenjp2",
- TIFF_ROOT=("libtiff-5", "libtiff-4"),
- ZLIB_ROOT="zlib",
- FREETYPE_ROOT="freetype2",
- HARFBUZZ_ROOT="harfbuzz",
- FRIBIDI_ROOT="fribidi",
- LCMS_ROOT="lcms2",
- IMAGEQUANT_ROOT="libimagequant",
- ).items():
+ for root_name, lib_name in {
+ "JPEG_ROOT": "libjpeg",
+ "JPEG2K_ROOT": "libopenjp2",
+ "TIFF_ROOT": ("libtiff-5", "libtiff-4"),
+ "ZLIB_ROOT": "zlib",
+ "FREETYPE_ROOT": "freetype2",
+ "HARFBUZZ_ROOT": "harfbuzz",
+ "FRIBIDI_ROOT": "fribidi",
+ "LCMS_ROOT": "lcms2",
+ "IMAGEQUANT_ROOT": "libimagequant",
+ }.items():
root = globals()[root_name]
if root is None and root_name in os.environ:
@@ -986,7 +986,6 @@ ext_modules = [
try:
setup(
- version=PILLOW_VERSION,
cmdclass={"build_ext": pil_build_ext},
ext_modules=ext_modules,
zip_safe=not (debug_build() or PLATFORM_MINGW),
diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py
index 161954831..e3eda4fe9 100644
--- a/src/PIL/BdfFontFile.py
+++ b/src/PIL/BdfFontFile.py
@@ -20,7 +20,9 @@
"""
Parse X Bitmap Distribution Format (BDF)
"""
+from __future__ import annotations
+from typing import BinaryIO
from . import FontFile, Image
@@ -36,7 +38,17 @@ bdf_slant = {
bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
-def bdf_char(f):
+def bdf_char(
+ f: BinaryIO,
+) -> (
+ tuple[
+ str,
+ int,
+ tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]],
+ Image.Image,
+ ]
+ | None
+):
# skip to STARTCHAR
while True:
s = f.readline()
@@ -56,13 +68,12 @@ def bdf_char(f):
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
# load bitmap
- bitmap = []
+ bitmap = bytearray()
while True:
s = f.readline()
if not s or s[:7] == b"ENDCHAR":
break
- bitmap.append(s[:-1])
- bitmap = b"".join(bitmap)
+ bitmap += s[:-1]
# The word BBX
# followed by the width in x (BBw), height in y (BBh),
@@ -92,7 +103,7 @@ def bdf_char(f):
class BdfFontFile(FontFile.FontFile):
"""Font file plugin for the X11 BDF format."""
- def __init__(self, fp):
+ def __init__(self, fp: BinaryIO):
super().__init__()
s = fp.readline()
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 398696d5c..b8f38b78a 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -28,6 +28,7 @@ BLP files come in many different flavours:
- DXT3 compression is used if alpha_encoding == 1.
- DXT5 compression is used if alpha_encoding == 7.
"""
+from __future__ import annotations
import os
import struct
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index 9abfd0b5b..6f730cfef 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -22,7 +22,7 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
import os
@@ -230,21 +230,21 @@ class BmpImageFile(ImageFile.ImageFile):
else:
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
- greyscale = True
+ grayscale = True
indices = (
(0, 255)
if file_info["colors"] == 2
else list(range(file_info["colors"]))
)
- # ----------------- Check if greyscale and ignore palette if so
+ # ----------------- Check if grayscale and ignore palette if so
for ind, val in enumerate(indices):
rgb = palette[ind * padding : ind * padding + 3]
if rgb != o8(val) * 3:
- greyscale = False
+ grayscale = False
- # ------- If all colors are grey, white or black, ditch palette
- if greyscale:
+ # ------- If all colors are gray, white or black, ditch palette
+ if grayscale:
self._mode = "1" if file_info["colors"] == 2 else "L"
raw_mode = self.mode
else:
@@ -396,7 +396,7 @@ def _save(im, fp, filename, bitmap_header=True):
dpi = info.get("dpi", (96, 96))
# 1 meter == 39.3701 inches
- ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi))
+ ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
header = 40 # or 64 for OS/2 version 2
diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py
index eef25aa14..60f3ec25b 100644
--- a/src/PIL/BufrStubImagePlugin.py
+++ b/src/PIL/BufrStubImagePlugin.py
@@ -8,6 +8,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
from . import Image, ImageFile
diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py
index 45e80b39a..0035296a4 100644
--- a/src/PIL/ContainerIO.py
+++ b/src/PIL/ContainerIO.py
@@ -13,18 +13,19 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
import io
+from typing import IO, AnyStr, Generic, Literal
-class ContainerIO:
+class ContainerIO(Generic[AnyStr]):
"""
A file object that provides read access to a part of an existing
file (for example a TAR file).
"""
- def __init__(self, file, offset, length):
+ def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None:
"""
Create file object.
@@ -32,7 +33,7 @@ class ContainerIO:
:param offset: Start of region, in bytes.
:param length: Size of region, in bytes.
"""
- self.fh = file
+ self.fh: IO[AnyStr] = file
self.pos = 0
self.offset = offset
self.length = length
@@ -41,10 +42,10 @@ class ContainerIO:
##
# Always false.
- def isatty(self):
+ def isatty(self) -> bool:
return False
- def seek(self, offset, mode=io.SEEK_SET):
+ def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None:
"""
Move file pointer.
@@ -63,7 +64,7 @@ class ContainerIO:
self.pos = max(0, min(self.pos, self.length))
self.fh.seek(self.offset + self.pos)
- def tell(self):
+ def tell(self) -> int:
"""
Get current file pointer.
@@ -71,7 +72,7 @@ class ContainerIO:
"""
return self.pos
- def read(self, n=0):
+ def read(self, n: int = 0) -> AnyStr:
"""
Read data.
@@ -84,17 +85,17 @@ class ContainerIO:
else:
n = self.length - self.pos
if not n: # EOF
- return b"" if "b" in self.fh.mode else ""
+ return b"" if "b" in self.fh.mode else "" # type: ignore[return-value]
self.pos = self.pos + n
return self.fh.read(n)
- def readline(self):
+ def readline(self) -> AnyStr:
"""
Read a line of text.
:returns: An 8-bit string.
"""
- s = b"" if "b" in self.fh.mode else ""
+ s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment]
newline_character = b"\n" if "b" in self.fh.mode else "\n"
while True:
c = self.read(1)
@@ -105,7 +106,7 @@ class ContainerIO:
break
return s
- def readlines(self):
+ def readlines(self) -> list[AnyStr]:
"""
Read multiple lines of text.
diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py
index 94efff341..5fb2b0193 100644
--- a/src/PIL/CurImagePlugin.py
+++ b/src/PIL/CurImagePlugin.py
@@ -15,6 +15,8 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
from . import BmpImagePlugin, Image
from ._binary import i16le as i16
from ._binary import i32le as i32
@@ -64,8 +66,6 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
d, e, o, a = self.tile[0]
self.tile[0] = d, (0, 0) + self.size, o, a
- return
-
#
# --------------------------------------------------------------------
diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py
index cde9d42f0..f7344df44 100644
--- a/src/PIL/DcxImagePlugin.py
+++ b/src/PIL/DcxImagePlugin.py
@@ -20,6 +20,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
from . import Image
from ._binary import i32le as i32
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index 54f358c7f..eb4c8f557 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -3,109 +3,323 @@ A Pillow loader for .dds files (S3TC-compressed aka DXTC)
Jerome Leclanche
Documentation:
- https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
+https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
- https://creativecommons.org/publicdomain/zero/1.0/
+https://creativecommons.org/publicdomain/zero/1.0/
"""
+from __future__ import annotations
+import io
import struct
-from io import BytesIO
+import sys
+from enum import IntEnum, IntFlag
from . import Image, ImageFile, ImagePalette
+from ._binary import i32le as i32
+from ._binary import o8
from ._binary import o32le as o32
# Magic ("DDS ")
DDS_MAGIC = 0x20534444
+
# DDS flags
-DDSD_CAPS = 0x1
-DDSD_HEIGHT = 0x2
-DDSD_WIDTH = 0x4
-DDSD_PITCH = 0x8
-DDSD_PIXELFORMAT = 0x1000
-DDSD_MIPMAPCOUNT = 0x20000
-DDSD_LINEARSIZE = 0x80000
-DDSD_DEPTH = 0x800000
+class DDSD(IntFlag):
+ CAPS = 0x1
+ HEIGHT = 0x2
+ WIDTH = 0x4
+ PITCH = 0x8
+ PIXELFORMAT = 0x1000
+ MIPMAPCOUNT = 0x20000
+ LINEARSIZE = 0x80000
+ DEPTH = 0x800000
+
# DDS caps
-DDSCAPS_COMPLEX = 0x8
-DDSCAPS_TEXTURE = 0x1000
-DDSCAPS_MIPMAP = 0x400000
+class DDSCAPS(IntFlag):
+ COMPLEX = 0x8
+ TEXTURE = 0x1000
+ MIPMAP = 0x400000
+
+
+class DDSCAPS2(IntFlag):
+ CUBEMAP = 0x200
+ CUBEMAP_POSITIVEX = 0x400
+ CUBEMAP_NEGATIVEX = 0x800
+ CUBEMAP_POSITIVEY = 0x1000
+ CUBEMAP_NEGATIVEY = 0x2000
+ CUBEMAP_POSITIVEZ = 0x4000
+ CUBEMAP_NEGATIVEZ = 0x8000
+ VOLUME = 0x200000
-DDSCAPS2_CUBEMAP = 0x200
-DDSCAPS2_CUBEMAP_POSITIVEX = 0x400
-DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800
-DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000
-DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000
-DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000
-DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000
-DDSCAPS2_VOLUME = 0x200000
# Pixel Format
-DDPF_ALPHAPIXELS = 0x1
-DDPF_ALPHA = 0x2
-DDPF_FOURCC = 0x4
-DDPF_PALETTEINDEXED8 = 0x20
-DDPF_RGB = 0x40
-DDPF_LUMINANCE = 0x20000
-
-
-# dds.h
-
-DDS_FOURCC = DDPF_FOURCC
-DDS_RGB = DDPF_RGB
-DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS
-DDS_LUMINANCE = DDPF_LUMINANCE
-DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS
-DDS_ALPHA = DDPF_ALPHA
-DDS_PAL8 = DDPF_PALETTEINDEXED8
-
-DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
-DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT
-DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH
-DDS_HEADER_FLAGS_PITCH = DDSD_PITCH
-DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE
-
-DDS_HEIGHT = DDSD_HEIGHT
-DDS_WIDTH = DDSD_WIDTH
-
-DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE
-DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP
-DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX
-
-DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX
-DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX
-DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY
-DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY
-DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ
-DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ
-
-
-# DXT1
-DXT1_FOURCC = 0x31545844
-
-# DXT3
-DXT3_FOURCC = 0x33545844
-
-# DXT5
-DXT5_FOURCC = 0x35545844
+class DDPF(IntFlag):
+ ALPHAPIXELS = 0x1
+ ALPHA = 0x2
+ FOURCC = 0x4
+ PALETTEINDEXED8 = 0x20
+ RGB = 0x40
+ LUMINANCE = 0x20000
# dxgiformat.h
+class DXGI_FORMAT(IntEnum):
+ UNKNOWN = 0
+ R32G32B32A32_TYPELESS = 1
+ R32G32B32A32_FLOAT = 2
+ R32G32B32A32_UINT = 3
+ R32G32B32A32_SINT = 4
+ R32G32B32_TYPELESS = 5
+ R32G32B32_FLOAT = 6
+ R32G32B32_UINT = 7
+ R32G32B32_SINT = 8
+ R16G16B16A16_TYPELESS = 9
+ R16G16B16A16_FLOAT = 10
+ R16G16B16A16_UNORM = 11
+ R16G16B16A16_UINT = 12
+ R16G16B16A16_SNORM = 13
+ R16G16B16A16_SINT = 14
+ R32G32_TYPELESS = 15
+ R32G32_FLOAT = 16
+ R32G32_UINT = 17
+ R32G32_SINT = 18
+ R32G8X24_TYPELESS = 19
+ D32_FLOAT_S8X24_UINT = 20
+ R32_FLOAT_X8X24_TYPELESS = 21
+ X32_TYPELESS_G8X24_UINT = 22
+ R10G10B10A2_TYPELESS = 23
+ R10G10B10A2_UNORM = 24
+ R10G10B10A2_UINT = 25
+ R11G11B10_FLOAT = 26
+ R8G8B8A8_TYPELESS = 27
+ R8G8B8A8_UNORM = 28
+ R8G8B8A8_UNORM_SRGB = 29
+ R8G8B8A8_UINT = 30
+ R8G8B8A8_SNORM = 31
+ R8G8B8A8_SINT = 32
+ R16G16_TYPELESS = 33
+ R16G16_FLOAT = 34
+ R16G16_UNORM = 35
+ R16G16_UINT = 36
+ R16G16_SNORM = 37
+ R16G16_SINT = 38
+ R32_TYPELESS = 39
+ D32_FLOAT = 40
+ R32_FLOAT = 41
+ R32_UINT = 42
+ R32_SINT = 43
+ R24G8_TYPELESS = 44
+ D24_UNORM_S8_UINT = 45
+ R24_UNORM_X8_TYPELESS = 46
+ X24_TYPELESS_G8_UINT = 47
+ R8G8_TYPELESS = 48
+ R8G8_UNORM = 49
+ R8G8_UINT = 50
+ R8G8_SNORM = 51
+ R8G8_SINT = 52
+ R16_TYPELESS = 53
+ R16_FLOAT = 54
+ D16_UNORM = 55
+ R16_UNORM = 56
+ R16_UINT = 57
+ R16_SNORM = 58
+ R16_SINT = 59
+ R8_TYPELESS = 60
+ R8_UNORM = 61
+ R8_UINT = 62
+ R8_SNORM = 63
+ R8_SINT = 64
+ A8_UNORM = 65
+ R1_UNORM = 66
+ R9G9B9E5_SHAREDEXP = 67
+ R8G8_B8G8_UNORM = 68
+ G8R8_G8B8_UNORM = 69
+ BC1_TYPELESS = 70
+ BC1_UNORM = 71
+ BC1_UNORM_SRGB = 72
+ BC2_TYPELESS = 73
+ BC2_UNORM = 74
+ BC2_UNORM_SRGB = 75
+ BC3_TYPELESS = 76
+ BC3_UNORM = 77
+ BC3_UNORM_SRGB = 78
+ BC4_TYPELESS = 79
+ BC4_UNORM = 80
+ BC4_SNORM = 81
+ BC5_TYPELESS = 82
+ BC5_UNORM = 83
+ BC5_SNORM = 84
+ B5G6R5_UNORM = 85
+ B5G5R5A1_UNORM = 86
+ B8G8R8A8_UNORM = 87
+ B8G8R8X8_UNORM = 88
+ R10G10B10_XR_BIAS_A2_UNORM = 89
+ B8G8R8A8_TYPELESS = 90
+ B8G8R8A8_UNORM_SRGB = 91
+ B8G8R8X8_TYPELESS = 92
+ B8G8R8X8_UNORM_SRGB = 93
+ BC6H_TYPELESS = 94
+ BC6H_UF16 = 95
+ BC6H_SF16 = 96
+ BC7_TYPELESS = 97
+ BC7_UNORM = 98
+ BC7_UNORM_SRGB = 99
+ AYUV = 100
+ Y410 = 101
+ Y416 = 102
+ NV12 = 103
+ P010 = 104
+ P016 = 105
+ OPAQUE_420 = 106
+ YUY2 = 107
+ Y210 = 108
+ Y216 = 109
+ NV11 = 110
+ AI44 = 111
+ IA44 = 112
+ P8 = 113
+ A8P8 = 114
+ B4G4R4A4_UNORM = 115
+ P208 = 130
+ V208 = 131
+ V408 = 132
+ SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189
+ SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190
-DXGI_FORMAT_R8G8B8A8_TYPELESS = 27
-DXGI_FORMAT_R8G8B8A8_UNORM = 28
-DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
-DXGI_FORMAT_BC5_TYPELESS = 82
-DXGI_FORMAT_BC5_UNORM = 83
-DXGI_FORMAT_BC5_SNORM = 84
-DXGI_FORMAT_BC6H_UF16 = 95
-DXGI_FORMAT_BC6H_SF16 = 96
-DXGI_FORMAT_BC7_TYPELESS = 97
-DXGI_FORMAT_BC7_UNORM = 98
-DXGI_FORMAT_BC7_UNORM_SRGB = 99
+
+class D3DFMT(IntEnum):
+ UNKNOWN = 0
+ R8G8B8 = 20
+ A8R8G8B8 = 21
+ X8R8G8B8 = 22
+ R5G6B5 = 23
+ X1R5G5B5 = 24
+ A1R5G5B5 = 25
+ A4R4G4B4 = 26
+ R3G3B2 = 27
+ A8 = 28
+ A8R3G3B2 = 29
+ X4R4G4B4 = 30
+ A2B10G10R10 = 31
+ A8B8G8R8 = 32
+ X8B8G8R8 = 33
+ G16R16 = 34
+ A2R10G10B10 = 35
+ A16B16G16R16 = 36
+ A8P8 = 40
+ P8 = 41
+ L8 = 50
+ A8L8 = 51
+ A4L4 = 52
+ V8U8 = 60
+ L6V5U5 = 61
+ X8L8V8U8 = 62
+ Q8W8V8U8 = 63
+ V16U16 = 64
+ A2W10V10U10 = 67
+ D16_LOCKABLE = 70
+ D32 = 71
+ D15S1 = 73
+ D24S8 = 75
+ D24X8 = 77
+ D24X4S4 = 79
+ D16 = 80
+ D32F_LOCKABLE = 82
+ D24FS8 = 83
+ D32_LOCKABLE = 84
+ S8_LOCKABLE = 85
+ L16 = 81
+ VERTEXDATA = 100
+ INDEX16 = 101
+ INDEX32 = 102
+ Q16W16V16U16 = 110
+ R16F = 111
+ G16R16F = 112
+ A16B16G16R16F = 113
+ R32F = 114
+ G32R32F = 115
+ A32B32G32R32F = 116
+ CxV8U8 = 117
+ A1 = 118
+ A2B10G10R10_XR_BIAS = 119
+ BINARYBUFFER = 199
+
+ UYVY = i32(b"UYVY")
+ R8G8_B8G8 = i32(b"RGBG")
+ YUY2 = i32(b"YUY2")
+ G8R8_G8B8 = i32(b"GRGB")
+ DXT1 = i32(b"DXT1")
+ DXT2 = i32(b"DXT2")
+ DXT3 = i32(b"DXT3")
+ DXT4 = i32(b"DXT4")
+ DXT5 = i32(b"DXT5")
+ DX10 = i32(b"DX10")
+ BC4S = i32(b"BC4S")
+ BC4U = i32(b"BC4U")
+ BC5S = i32(b"BC5S")
+ BC5U = i32(b"BC5U")
+ ATI1 = i32(b"ATI1")
+ ATI2 = i32(b"ATI2")
+ MULTI2_ARGB8 = i32(b"MET1")
+
+
+# Backward compatibility layer
+module = sys.modules[__name__]
+for item in DDSD:
+ setattr(module, "DDSD_" + item.name, item.value)
+for item in DDSCAPS:
+ setattr(module, "DDSCAPS_" + item.name, item.value)
+for item in DDSCAPS2:
+ setattr(module, "DDSCAPS2_" + item.name, item.value)
+for item in DDPF:
+ setattr(module, "DDPF_" + item.name, item.value)
+
+DDS_FOURCC = DDPF.FOURCC
+DDS_RGB = DDPF.RGB
+DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS
+DDS_LUMINANCE = DDPF.LUMINANCE
+DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS
+DDS_ALPHA = DDPF.ALPHA
+DDS_PAL8 = DDPF.PALETTEINDEXED8
+
+DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT
+DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT
+DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH
+DDS_HEADER_FLAGS_PITCH = DDSD.PITCH
+DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE
+
+DDS_HEIGHT = DDSD.HEIGHT
+DDS_WIDTH = DDSD.WIDTH
+
+DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE
+DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP
+DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX
+
+DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX
+DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX
+DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY
+DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY
+DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ
+DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ
+
+DXT1_FOURCC = D3DFMT.DXT1
+DXT3_FOURCC = D3DFMT.DXT3
+DXT5_FOURCC = D3DFMT.DXT5
+
+DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS
+DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM
+DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB
+DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS
+DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM
+DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM
+DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16
+DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16
+DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS
+DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM
+DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB
class DdsImageFile(ImageFile.ImageFile):
@@ -124,166 +338,222 @@ class DdsImageFile(ImageFile.ImageFile):
if len(header_bytes) != 120:
msg = f"Incomplete header: {len(header_bytes)} bytes"
raise OSError(msg)
- header = BytesIO(header_bytes)
+ header = io.BytesIO(header_bytes)
flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height)
- self._mode = "RGBA"
+ extents = (0, 0) + self.size
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved
# pixel format
- pfsize, pfflags = struct.unpack("<2I", header.read(8))
- fourcc = header.read(4)
- (bitcount,) = struct.unpack("> (offset + 1) << (offset + 1) == mask:
+ offset += 1
+ mask_offsets.append(offset)
+ mask_totals.append(mask >> offset)
+
+ data = bytearray()
+ bytecount = bitcount // 8
+ while len(data) < self.state.xsize * self.state.ysize * len(masks):
+ value = int.from_bytes(self.fd.read(bytecount), "little")
+ for i, mask in enumerate(masks):
+ masked_value = value & mask
+ # Remove the zero padding, and scale it to 8 bits
+ data += o8(
+ int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
+ )
+ self.set_as_raw(bytes(data))
+ return -1, 0
+
+
def _save(im, fp, filename):
if im.mode not in ("RGB", "RGBA", "L", "LA"):
msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg)
- rawmode = im.mode
- masks = [0xFF0000, 0xFF00, 0xFF]
- if im.mode in ("L", "LA"):
- pixel_flags = DDPF_LUMINANCE
+ 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 = rawmode[::-1]
- if im.mode in ("LA", "RGBA"):
- pixel_flags |= DDPF_ALPHAPIXELS
- masks.append(0xFF000000)
+ pixel_flags = DDPF.RGB
+ rawmode = im.mode[::-1]
+ rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
- bitcount = len(masks) * 8
- while len(masks) < 4:
- masks.append(0)
+ 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
+ bitcount = len(im.getbands()) * 8
+ pitch = (im.width * bitcount + 7) // 8
fp.write(
o32(DDS_MAGIC)
- + o32(124) # header size
- + o32(
- DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PITCH | DDSD_PIXELFORMAT
- ) # flags
- + o32(im.height)
- + o32(im.width)
- + o32((im.width * bitcount + 7) // 8) # pitch
- + o32(0) # depth
- + o32(0) # mipmaps
- + o32(0) * 11 # reserved
- + o32(32) # pfsize
- + o32(pixel_flags) # pfflags
- + o32(0) # fourcc
- + o32(bitcount) # bitcount
- + b"".join(o32(mask) for mask in masks) # rgbabitmask
- + o32(DDSCAPS_TEXTURE) # dwCaps
- + o32(0) # dwCaps2
- + o32(0) # dwCaps3
- + o32(0) # dwCaps4
- + o32(0) # dwReserved2
+ + struct.pack(
+ "<7I",
+ 124, # header size
+ flags, # flags
+ im.height,
+ im.width,
+ pitch,
+ 0, # depth
+ 0, # mipmaps
+ )
+ + struct.pack("11I", *((0,) * 11)) # reserved
+ # pfsize, pfflags, fourcc, bitcount
+ + struct.pack("<4I", 32, pixel_flags, 0, 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, 0, 1))]
)
- if im.mode == "RGBA":
- r, g, b, a = im.split()
- im = Image.merge("RGBA", (a, r, g, b))
- ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
def _accept(prefix):
@@ -291,5 +561,6 @@ def _accept(prefix):
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
+Image.register_decoder("dds_rgb", DdsRgbDecoder)
Image.register_save(DdsImageFile.format, _save)
Image.register_extension(DdsImageFile.format, ".dds")
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 9b2fce0ac..d2e60aa07 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -19,6 +19,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import io
import os
@@ -77,14 +78,11 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
# Hack to support hi-res rendering
scale = int(scale) or 1
- # orig_size = size
- # orig_bbox = bbox
- size = (size[0] * scale, size[1] * scale)
+ width = size[0] * scale
+ height = size[1] * scale
# resolution is dependent on bbox and size
- res = (
- 72.0 * size[0] / (bbox[2] - bbox[0]),
- 72.0 * size[1] / (bbox[3] - bbox[1]),
- )
+ res_x = 72.0 * width / (bbox[2] - bbox[0])
+ res_y = 72.0 * height / (bbox[3] - bbox[1])
out_fd, outfile = tempfile.mkstemp()
os.close(out_fd)
@@ -121,8 +119,8 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
command = [
gs_binary,
"-q", # quiet mode
- "-g%dx%d" % size, # set output geometry (pixels)
- "-r%fx%f" % res, # set input DPI (dots per inch)
+ f"-g{width:d}x{height:d}", # set output geometry (pixels)
+ f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch)
"-dBATCH", # exit after processing
"-dNOPAUSE", # don't pause between pages
"-dSAFER", # safe mode
diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py
index 2347c6d4c..60a4d9774 100644
--- a/src/PIL/ExifTags.py
+++ b/src/PIL/ExifTags.py
@@ -13,6 +13,7 @@
This module provides constants and clear-text names for various
well-known EXIF tags.
"""
+from __future__ import annotations
from enum import IntEnum
diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index e0e51aaac..e69890bab 100644
--- a/src/PIL/FitsImagePlugin.py
+++ b/src/PIL/FitsImagePlugin.py
@@ -8,13 +8,14 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import math
from . import Image, ImageFile
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:6] == b"SIMPLE"
@@ -22,8 +23,10 @@ class FitsImageFile(ImageFile.ImageFile):
format = "FITS"
format_description = "FITS"
- def _open(self):
- headers = {}
+ def _open(self) -> None:
+ assert self.fp is not None
+
+ headers: dict[bytes, bytes] = {}
while True:
header = self.fp.read(80)
if not header:
@@ -54,12 +57,10 @@ class FitsImageFile(ImageFile.ImageFile):
self._mode = "L"
elif number_of_bits == 16:
self._mode = "I"
- # rawmode = "I;16S"
elif number_of_bits == 32:
self._mode = "I"
elif number_of_bits in (-32, -64):
self._mode = "F"
- # rawmode = "F" if number_of_bits == -32 else "F;64F"
offset = math.ceil(self.fp.tell() / 2880) * 2880
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index 8f641ece9..9769761fc 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -14,6 +14,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import os
@@ -150,7 +151,8 @@ class FliImageFile(ImageFile.ImageFile):
s = self.fp.read(4)
if not s:
- raise EOFError
+ msg = "missing frame size"
+ raise EOFError(msg)
framesize = i32(s)
diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py
index 5ec0a6632..3ec1ae819 100644
--- a/src/PIL/FontFile.py
+++ b/src/PIL/FontFile.py
@@ -13,16 +13,19 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
import os
+from typing import BinaryIO
from . import Image, _binary
WIDTH = 800
-def puti16(fp, values):
+def puti16(
+ fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int]
+) -> None:
"""Write network order (big-endian) 16-bit sequence"""
for v in values:
if v < 0:
@@ -33,16 +36,34 @@ def puti16(fp, values):
class FontFile:
"""Base class for raster font file handlers."""
- bitmap = None
+ bitmap: Image.Image | None = None
- def __init__(self):
- self.info = {}
- self.glyph = [None] * 256
+ def __init__(self) -> None:
+ self.info: dict[bytes, bytes | int] = {}
+ self.glyph: list[
+ tuple[
+ tuple[int, int],
+ tuple[int, int, int, int],
+ tuple[int, int, int, int],
+ Image.Image,
+ ]
+ | None
+ ] = [None] * 256
- def __getitem__(self, ix):
+ def __getitem__(
+ self, ix: int
+ ) -> (
+ tuple[
+ tuple[int, int],
+ tuple[int, int, int, int],
+ tuple[int, int, int, int],
+ Image.Image,
+ ]
+ | None
+ ):
return self.glyph[ix]
- def compile(self):
+ def compile(self) -> None:
"""Create metrics and bitmap"""
if self.bitmap:
@@ -51,7 +72,7 @@ class FontFile:
# create bitmap large enough to hold all data
h = w = maxwidth = 0
lines = 1
- for glyph in self:
+ for glyph in self.glyph:
if glyph:
d, dst, src, im = glyph
h = max(h, src[3] - src[1])
@@ -65,20 +86,22 @@ class FontFile:
ysize = lines * h
if xsize == 0 and ysize == 0:
- return ""
+ return
self.ysize = h
# paste glyphs into bitmap
self.bitmap = Image.new("1", (xsize, ysize))
- self.metrics = [None] * 256
+ self.metrics: list[
+ tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]]
+ | None
+ ] = [None] * 256
x = y = 0
for i in range(256):
glyph = self[i]
if glyph:
d, dst, src, im = glyph
xx = src[2] - src[0]
- # yy = src[3] - src[1]
x0, y0 = x, y
x = x + xx
if x > WIDTH:
@@ -89,12 +112,15 @@ class FontFile:
self.bitmap.paste(im.crop(src), s)
self.metrics[i] = d, dst, s
- def save(self, filename):
+ def save(self, filename: str) -> None:
"""Save font"""
self.compile()
# font data
+ if not self.bitmap:
+ msg = "No bitmap created"
+ raise ValueError(msg)
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
# font metrics
@@ -105,6 +131,6 @@ class FontFile:
for id in range(256):
m = self.metrics[id]
if not m:
- puti16(fp, [0] * 10)
+ puti16(fp, (0,) * 10)
else:
puti16(fp, m[0] + m[1] + m[2])
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index a878cbfd2..75680a94e 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -14,6 +14,8 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
import olefile
from . import Image, ImageFile
@@ -97,16 +99,15 @@ class FpxImageFile(ImageFile.ImageFile):
s = prop[0x2000002 | id]
- colors = []
bands = i32(s, 4)
if bands > 4:
msg = "Invalid number of bands"
raise OSError(msg)
- for i in range(bands):
- # note: for now, we ignore the "uncalibrated" flag
- colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
- self._mode, self.rawmode = MODES[tuple(colors)]
+ # note: for now, we ignore the "uncalibrated" flag
+ colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands))
+
+ self._mode, self.rawmode = MODES[colors]
# load JPEG tables, if any
self.jpeg = {}
@@ -227,6 +228,7 @@ class FpxImageFile(ImageFile.ImageFile):
break # isn't really required
self.stream = stream
+ self._fp = self.fp
self.fp = None
def load(self):
diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py
index c2e4ead71..d5513a56a 100644
--- a/src/PIL/FtexImagePlugin.py
+++ b/src/PIL/FtexImagePlugin.py
@@ -50,6 +50,7 @@ bytes for that mipmap level.
Note: All data is stored in little-Endian (Intel) byte order.
"""
+from __future__ import annotations
import struct
from enum import IntEnum
diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py
index ec6e9de6e..6722fa2b1 100644
--- a/src/PIL/GbrImagePlugin.py
+++ b/src/PIL/GbrImagePlugin.py
@@ -23,6 +23,7 @@
# Version 2 files are saved by GIMP v2.8 (at least)
# Version 3 files have a format specifier of 18 for 16bit floats in
# the color depth field. This is currently unsupported by Pillow.
+from __future__ import annotations
from . import Image, ImageFile
from ._binary import i32be as i32
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
index 3599994a8..7bb4736af 100644
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -25,7 +25,9 @@
implementation is provided for convenience and demonstrational
purposes only.
"""
+from __future__ import annotations
+from io import BytesIO
from . import ImageFile, ImagePalette, UnidentifiedImageError
from ._binary import i16be as i16
@@ -43,8 +45,10 @@ class GdImageFile(ImageFile.ImageFile):
format = "GD"
format_description = "GD uncompressed images"
- def _open(self):
+ def _open(self) -> None:
# Header
+ assert self.fp is not None
+
s = self.fp.read(1037)
if i16(s) not in [65534, 65535]:
@@ -76,7 +80,7 @@ class GdImageFile(ImageFile.ImageFile):
]
-def open(fp, mode="r"):
+def open(fp: BytesIO, mode: str = "r") -> GdImageFile:
"""
Load texture from a GD image file.
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 92074b0d4..57d87078b 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -23,6 +23,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import itertools
import math
@@ -30,7 +31,15 @@ import os
import subprocess
from enum import IntEnum
-from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
+from . import (
+ Image,
+ ImageChops,
+ ImageFile,
+ ImageMath,
+ ImageOps,
+ ImagePalette,
+ ImageSequence,
+)
from ._binary import i16le as i16
from ._binary import o8
from ._binary import o16le as o16
@@ -183,7 +192,8 @@ class GifImageFile(ImageFile.ImageFile):
s = self.fp.read(1)
if not s or s == b";":
- raise EOFError
+ msg = "no more images in GIF file"
+ raise EOFError(msg)
palette = None
@@ -280,15 +290,11 @@ class GifImageFile(ImageFile.ImageFile):
bits = self.fp.read(1)[0]
self.__offset = self.fp.tell()
break
-
- else:
- pass
- # raise OSError, "illegal GIF tag `%x`" % s[0]
s = None
if interlace is None:
- # self._fp = None
- raise EOFError
+ msg = "image not found in GIF frame"
+ raise EOFError(msg)
self.__frame = frame
if not update_image:
@@ -333,6 +339,8 @@ class GifImageFile(ImageFile.ImageFile):
def _rgb(color):
if self._frame_palette:
+ if color * 3 + 3 > len(self._frame_palette.palette):
+ color = 0
color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
else:
color = (color, color, color)
@@ -537,7 +545,15 @@ def _normalize_palette(im, palette, info):
else:
used_palette_colors = _get_optimize(im, info)
if used_palette_colors is not None:
- return im.remap_palette(used_palette_colors, source_palette)
+ im = im.remap_palette(used_palette_colors, source_palette)
+ if "transparency" in info:
+ try:
+ info["transparency"] = used_palette_colors.index(
+ info["transparency"]
+ )
+ except ValueError:
+ del info["transparency"]
+ return im
im.palette.palette = source_palette
return im
@@ -565,13 +581,11 @@ def _write_single_frame(im, fp, palette):
def _getbbox(base_im, im_frame):
- if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):
- delta = ImageChops.subtract_modulo(im_frame, base_im)
- else:
- delta = ImageChops.subtract_modulo(
- im_frame.convert("RGBA"), base_im.convert("RGBA")
- )
- return delta.getbbox(alpha_only=False)
+ if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im):
+ im_frame = im_frame.convert("RGBA")
+ base_im = base_im.convert("RGBA")
+ delta = ImageChops.subtract_modulo(im_frame, base_im)
+ return delta, delta.getbbox(alpha_only=False)
def _write_multiple_frames(im, fp, palette):
@@ -579,6 +593,7 @@ def _write_multiple_frames(im, fp, palette):
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
im_frames = []
+ previous_im = None
frame_count = 0
background_im = None
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
@@ -592,9 +607,9 @@ def _write_multiple_frames(im, fp, palette):
im.encoderinfo.setdefault(k, v)
encoderinfo = im.encoderinfo.copy()
- im_frame = _normalize_palette(im_frame, palette, encoderinfo)
if "transparency" in im_frame.info:
encoderinfo.setdefault("transparency", im_frame.info["transparency"])
+ im_frame = _normalize_palette(im_frame, palette, encoderinfo)
if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count]
elif duration is None and "duration" in im_frame.info:
@@ -603,14 +618,16 @@ def _write_multiple_frames(im, fp, palette):
encoderinfo["disposal"] = disposal[frame_count]
frame_count += 1
+ diff_frame = None
if im_frames:
# delta frame
- previous = im_frames[-1]
- bbox = _getbbox(previous["im"], im_frame)
+ delta, bbox = _getbbox(previous_im, im_frame)
if not bbox:
# This frame is identical to the previous frame
if encoderinfo.get("duration"):
- previous["encoderinfo"]["duration"] += encoderinfo["duration"]
+ im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[
+ "duration"
+ ]
continue
if encoderinfo.get("disposal") == 2:
if background_im is None:
@@ -620,33 +637,67 @@ def _write_multiple_frames(im, fp, palette):
background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background)
background_im.putpalette(im_frames[0]["im"].palette)
- bbox = _getbbox(background_im, im_frame)
+ delta, bbox = _getbbox(background_im, im_frame)
+ if encoderinfo.get("optimize") and im_frame.mode != "1":
+ if "transparency" not in encoderinfo:
+ try:
+ encoderinfo[
+ "transparency"
+ ] = im_frame.palette._new_color_index(im_frame)
+ except ValueError:
+ pass
+ if "transparency" in encoderinfo:
+ # When the delta is zero, fill the image with transparency
+ diff_frame = im_frame.copy()
+ fill = Image.new(
+ "P", diff_frame.size, encoderinfo["transparency"]
+ )
+ if delta.mode == "RGBA":
+ r, g, b, a = delta.split()
+ mask = ImageMath.eval(
+ "convert(max(max(max(r, g), b), a) * 255, '1')",
+ r=r,
+ g=g,
+ b=b,
+ a=a,
+ )
+ else:
+ if delta.mode == "P":
+ # Convert to L without considering palette
+ delta_l = Image.new("L", delta.size)
+ delta_l.putdata(delta.getdata())
+ delta = delta_l
+ mask = ImageMath.eval("convert(im * 255, '1')", im=delta)
+ diff_frame.paste(fill, mask=ImageOps.invert(mask))
else:
bbox = None
- im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
+ previous_im = im_frame
+ im_frames.append(
+ {"im": diff_frame or im_frame, "bbox": bbox, "encoderinfo": encoderinfo}
+ )
- if len(im_frames) > 1:
- for frame_data in im_frames:
- im_frame = frame_data["im"]
- if not frame_data["bbox"]:
- # global header
- for s in _get_global_header(im_frame, frame_data["encoderinfo"]):
- fp.write(s)
- offset = (0, 0)
- else:
- # compress difference
- if not palette:
- frame_data["encoderinfo"]["include_color_table"] = True
+ if len(im_frames) == 1:
+ if "duration" in im.encoderinfo:
+ # Since multiple frames will not be written, use the combined duration
+ im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"]
+ return
- im_frame = im_frame.crop(frame_data["bbox"])
- offset = frame_data["bbox"][:2]
- _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
- return True
- elif "duration" in im.encoderinfo and isinstance(
- im.encoderinfo["duration"], (list, tuple)
- ):
- # Since multiple frames will not be written, add together the frame durations
- im.encoderinfo["duration"] = sum(im.encoderinfo["duration"])
+ for frame_data in im_frames:
+ im_frame = frame_data["im"]
+ if not frame_data["bbox"]:
+ # global header
+ for s in _get_global_header(im_frame, frame_data["encoderinfo"]):
+ fp.write(s)
+ offset = (0, 0)
+ else:
+ # compress difference
+ if not palette:
+ frame_data["encoderinfo"]["include_color_table"] = True
+
+ im_frame = im_frame.crop(frame_data["bbox"])
+ offset = frame_data["bbox"][:2]
+ _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
+ return True
def _save_all(im, fp, filename):
@@ -659,7 +710,7 @@ def _save(im, fp, filename, save_all=False):
palette = im.encoderinfo.get("palette", im.info.get("palette"))
else:
palette = None
- im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
+ im.encoderinfo.setdefault("optimize", True)
if not save_all or not _write_multiple_frames(im, fp, palette):
_write_single_frame(im, fp, palette)
@@ -681,22 +732,10 @@ def get_interlace(im):
def _write_local_header(fp, im, offset, flags):
- transparent_color_exists = False
try:
- transparency = int(im.encoderinfo["transparency"])
- except (KeyError, ValueError):
- pass
- else:
- # optimize the block away if transparent color is not used
- transparent_color_exists = True
-
- used_palette_colors = _get_optimize(im, im.encoderinfo)
- if used_palette_colors is not None:
- # adjust the transparency index after optimize
- try:
- transparency = used_palette_colors.index(transparency)
- except ValueError:
- transparent_color_exists = False
+ transparency = im.encoderinfo["transparency"]
+ except KeyError:
+ transparency = None
if "duration" in im.encoderinfo:
duration = int(im.encoderinfo["duration"] / 10)
@@ -705,11 +744,9 @@ def _write_local_header(fp, im, offset, flags):
disposal = int(im.encoderinfo.get("disposal", 0))
- if transparent_color_exists or duration != 0 or disposal:
- packed_flag = 1 if transparent_color_exists else 0
+ if transparency is not None or duration != 0 or disposal:
+ packed_flag = 1 if transparency is not None else 0
packed_flag |= disposal << 2
- if not transparent_color_exists:
- transparency = 0
fp.write(
b"!"
@@ -717,7 +754,7 @@ def _write_local_header(fp, im, offset, flags):
+ o8(4) # length
+ o8(packed_flag) # packed fields
+ o16(duration) # duration
- + o8(transparency) # transparency index
+ + o8(transparency or 0) # transparency index
+ o8(0)
)
@@ -805,7 +842,7 @@ def _get_optimize(im, info):
:param info: encoderinfo
:returns: list of indexes of palette entries in use, or None
"""
- if im.mode in ("P", "L") and info and info.get("optimize", 0):
+ if im.mode in ("P", "L") and info and info.get("optimize"):
# Potentially expensive operation.
# The palette saves 3 bytes per color not used, but palette
diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py
index 8e801be0b..2d8c78ea9 100644
--- a/src/PIL/GimpGradientFile.py
+++ b/src/PIL/GimpGradientFile.py
@@ -18,7 +18,7 @@ Stuff to translate curve segments to palette values (derived from
the corresponding code in GIMP, written by Federico Mena Quintero.
See the GIMP distribution for more information.)
"""
-
+from __future__ import annotations
from math import log, pi, sin, sqrt
diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py
index d38892894..a3109ebaa 100644
--- a/src/PIL/GimpPaletteFile.py
+++ b/src/PIL/GimpPaletteFile.py
@@ -13,6 +13,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import re
diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py
index c1c71da08..f8106800c 100644
--- a/src/PIL/GribStubImagePlugin.py
+++ b/src/PIL/GribStubImagePlugin.py
@@ -8,6 +8,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
from . import Image, ImageFile
diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py
index c26b480ac..65409e269 100644
--- a/src/PIL/Hdf5StubImagePlugin.py
+++ b/src/PIL/Hdf5StubImagePlugin.py
@@ -8,6 +8,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
from . import Image, ImageFile
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index 0aa4f7a84..d877b4ecb 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -16,6 +16,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import io
import os
@@ -391,8 +392,8 @@ if __name__ == "__main__":
with open(sys.argv[1], "rb") as fp:
imf = IcnsImageFile(fp)
for size in imf.info["sizes"]:
- imf.size = size
- imf.save("out-%s-%s-%s.png" % size)
+ width, height, scale = imf.size = size
+ imf.save(f"out-{width}-{height}-{scale}.png")
with Image.open(sys.argv[1]) as im:
im.save("out.png")
if sys.platform == "windows":
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index 0445a2ab2..1b22f8645 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -20,7 +20,7 @@
# Icon format references:
# * https://en.wikipedia.org/wiki/ICO_(file_format)
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
-
+from __future__ import annotations
import warnings
from io import BytesIO
@@ -174,9 +174,7 @@ class IcoFile:
self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
# ICO images are usually squares
- # self.entry = sorted(self.entry, key=lambda x: x['width'])
- self.entry = sorted(self.entry, key=lambda x: x["square"])
- self.entry.reverse()
+ self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True)
def sizes(self):
"""
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index b42ba7cac..97d726a8a 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -24,7 +24,7 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
import os
import re
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 6867cfc9b..ef06839e3 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -24,6 +24,8 @@
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
import atexit
import builtins
import io
@@ -40,7 +42,7 @@ from enum import IntEnum
from pathlib import Path
try:
- import defusedxml.ElementTree as ElementTree
+ from defusedxml import ElementTree
except ImportError:
ElementTree = None
@@ -90,7 +92,7 @@ try:
raise ImportError(msg)
except ImportError as v:
- core = DeferredError(ImportError("The _imaging C module is not installed."))
+ core = DeferredError.new(ImportError("The _imaging C module is not installed."))
# Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for
@@ -240,7 +242,7 @@ MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "
_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B")
-def getmodebase(mode):
+def getmodebase(mode: str) -> str:
"""
Gets the "base" mode for given mode. This function returns "L" for
images that contain grayscale data, and "RGB" for images that
@@ -280,7 +282,7 @@ def getmodebandnames(mode):
return ImageMode.getmode(mode).bands
-def getmodebands(mode):
+def getmodebands(mode: str) -> int:
"""
Gets the number of individual bands for this mode.
@@ -355,10 +357,11 @@ def init():
if _initialized >= 2:
return 0
+ parent_name = __name__.rpartition(".")[0]
for plugin in _plugins:
try:
logger.debug("Importing %s", plugin)
- __import__(f"PIL.{plugin}", globals(), locals(), [])
+ __import__(f"{parent_name}.{plugin}", globals(), locals(), [])
except ImportError as e:
logger.debug("Image: failed to import %s: %s", plugin, e)
@@ -476,8 +479,8 @@ class Image:
* :py:func:`~PIL.Image.frombytes`
"""
- format = None
- format_description = None
+ format: str | None = None
+ format_description: str | None = None
_close_exclusive_fp_after_loading = True
def __init__(self):
@@ -537,15 +540,19 @@ class Image:
def __enter__(self):
return self
+ def _close_fp(self):
+ if getattr(self, "_fp", False):
+ if self._fp != self.fp:
+ self._fp.close()
+ self._fp = DeferredError(ValueError("Operation on closed image"))
+ if self.fp:
+ self.fp.close()
+
def __exit__(self, *args):
- if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False):
- if getattr(self, "_fp", False):
- if self._fp != self.fp:
- self._fp.close()
- self._fp = DeferredError(ValueError("Operation on closed image"))
- if self.fp:
- self.fp.close()
- self.fp = None
+ if hasattr(self, "fp"):
+ if getattr(self, "_exclusive_fp", False):
+ self._close_fp()
+ self.fp = None
def close(self):
"""
@@ -559,16 +566,12 @@ class Image:
:py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
more information.
"""
- try:
- if getattr(self, "_fp", False):
- if self._fp != self.fp:
- self._fp.close()
- self._fp = DeferredError(ValueError("Operation on closed image"))
- if self.fp:
- self.fp.close()
- self.fp = None
- except Exception as msg:
- logger.debug("Error closing: %s", msg)
+ if hasattr(self, "fp"):
+ try:
+ self._close_fp()
+ self.fp = None
+ except Exception as msg:
+ logger.debug("Error closing: %s", msg)
if getattr(self, "map", None):
self.map = None
@@ -590,7 +593,9 @@ class Image:
else:
self.load()
- def _dump(self, file=None, format=None, **options):
+ def _dump(
+ self, file: str | None = None, format: str | None = None, **options
+ ) -> str:
suffix = ""
if format:
suffix = "." + format
@@ -715,7 +720,7 @@ class Image:
self.putpalette(palette)
self.frombytes(data)
- def tobytes(self, encoder_name="raw", *args):
+ def tobytes(self, encoder_name: str = "raw", *args) -> bytes:
"""
Return image as a bytes object.
@@ -793,7 +798,7 @@ class Image:
]
)
- def frombytes(self, data, decoder_name="raw", *args):
+ def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None:
"""
Loads this image with pixel data from a bytes object.
@@ -801,6 +806,9 @@ class Image:
but loads data into this image instead of creating a new image object.
"""
+ if self.width == 0 or self.height == 0:
+ return
+
# may pass tuple instead of argument list
if len(args) == 1 and isinstance(args[0], tuple):
args = args[0]
@@ -877,7 +885,7 @@ class Image:
def convert(
self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256
- ):
+ ) -> Image:
"""
Returns a converted copy of this image. For the "P" mode, this
method translates pixels through the palette. If mode is
@@ -888,12 +896,12 @@ class Image:
"L", "RGB" and "CMYK". The ``matrix`` argument only supports "L"
and "RGB".
- When translating a color image to greyscale (mode "L"),
+ When translating a color image to grayscale (mode "L"),
the library uses the ITU-R 601-2 luma transform::
L = R * 299/1000 + G * 587/1000 + B * 114/1000
- The default method of converting a greyscale ("L") or "RGB"
+ The default method of converting a grayscale ("L") or "RGB"
image into a bilevel (mode "1") image uses Floyd-Steinberg
dither to approximate the original image luminosity levels. If
dither is ``None``, all values larger than 127 are set to 255 (white),
@@ -1166,7 +1174,7 @@ class Image:
if palette.mode != "P":
msg = "bad mode for palette image"
raise ValueError(msg)
- if self.mode != "RGB" and self.mode != "L":
+ if self.mode not in {"RGB", "L"}:
msg = "only RGB or L mode images can be quantized to a palette"
raise ValueError(msg)
im = self.im.convert("P", dither, palette.im)
@@ -1184,7 +1192,7 @@ class Image:
return im
- def copy(self):
+ def copy(self) -> Image:
"""
Copies this image. Use this method if you wish to paste things
into an image, but still retain the original.
@@ -1197,7 +1205,7 @@ class Image:
__copy__ = copy
- def crop(self, box=None):
+ def crop(self, box=None) -> Image:
"""
Returns a rectangular region from this image. The box is a
4-tuple defining the left, upper, right, and lower pixel
@@ -1248,7 +1256,7 @@ class Image:
Configures the image file loader so it returns a version of the
image that as closely as possible matches the given mode and
size. For example, you can use this method to convert a color
- JPEG to greyscale while loading it.
+ JPEG to grayscale while loading it.
If any changes are made, returns a tuple with the chosen ``mode`` and
``box`` with coordinates of the original image within the altered one.
@@ -1294,12 +1302,12 @@ class Image:
if self.im.bands == 1 or multiband:
return self._new(filter.filter(self.im))
- ims = []
- for c in range(self.im.bands):
- ims.append(self._new(filter.filter(self.im.getband(c))))
+ ims = [
+ self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands)
+ ]
return merge(self.mode, ims)
- def getbands(self):
+ def getbands(self) -> tuple[str, ...]:
"""
Returns a tuple containing the name of each band in this image.
For example, ``getbands`` on an RGB image returns ("R", "G", "B").
@@ -1309,7 +1317,7 @@ class Image:
"""
return ImageMode.getmode(self.mode).bands
- def getbbox(self, *, alpha_only=True):
+ def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]:
"""
Calculates the bounding box of the non-zero regions in the
image.
@@ -1345,10 +1353,7 @@ class Image:
self.load()
if self.mode in ("1", "L", "P"):
h = self.im.histogram()
- out = []
- for i in range(256):
- if h[i]:
- out.append((h[i], i))
+ out = [(h[i], i) for i in range(256) if h[i]]
if len(out) > maxcolors:
return None
return out
@@ -1389,10 +1394,7 @@ class Image:
self.load()
if self.im.bands > 1:
- extrema = []
- for i in range(self.im.bands):
- extrema.append(self.im.getband(i).getextrema())
- return tuple(extrema)
+ return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
return self.im.getextrema()
def _getxmp(self, xmp_tags):
@@ -1620,13 +1622,13 @@ class Image:
than one band, the histograms for all bands are concatenated (for
example, the histogram for an "RGB" image contains 768 values).
- A bilevel image (mode "1") is treated as a greyscale ("L") image
+ A bilevel image (mode "1") is treated as a grayscale ("L") image
by this method.
If a mask is provided, the method returns a histogram for those
parts of the image where the mask image is non-zero. The mask
image must have the same size as the image, and be either a
- bi-level image (mode "1") or a greyscale image ("L").
+ bi-level image (mode "1") or a grayscale image ("L").
:param mask: An optional mask.
:param extrema: An optional tuple of manually-specified extrema.
@@ -1646,13 +1648,13 @@ class Image:
"""
Calculates and returns the entropy for the image.
- A bilevel image (mode "1") is treated as a greyscale ("L")
+ A bilevel image (mode "1") is treated as a grayscale ("L")
image by this method.
If a mask is provided, the method employs the histogram for
those parts of the image where the mask image is non-zero.
The mask image must have the same size as the image, and be
- either a bi-level image (mode "1") or a greyscale image ("L").
+ either a bi-level image (mode "1") or a grayscale image ("L").
:param mask: An optional mask.
:param extrema: An optional tuple of manually-specified extrema.
@@ -1668,7 +1670,7 @@ class Image:
return self.im.entropy(extrema)
return self.im.entropy()
- def paste(self, im, box=None, mask=None):
+ def paste(self, im, box=None, mask=None) -> None:
"""
Pastes another image into this image. The box argument is either
a 2-tuple giving the upper left corner, a 4-tuple defining the
@@ -1872,7 +1874,8 @@ class Image:
# do things the hard way
im = self.im.convert(mode)
if im.mode not in ("LA", "PA", "RGBA"):
- raise ValueError from e # sanity check
+ msg = "alpha channel could not be added"
+ raise ValueError(msg) from e # sanity check
self.im = im
self.pyaccess = None
self._mode = self.im.mode
@@ -2360,7 +2363,7 @@ class Image:
(w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor
)
- def save(self, fp, format=None, **params):
+ def save(self, fp, format=None, **params) -> None:
"""
Saves this image under the given filename. If no format is
specified, the format to use is determined from the filename
@@ -2458,7 +2461,7 @@ class Image:
if open_fp:
fp.close()
- def seek(self, frame):
+ def seek(self, frame) -> Image:
"""
Seeks to the given frame in this sequence file. If you seek
beyond the end of the sequence, the method raises an
@@ -2477,7 +2480,8 @@ class Image:
# overridden by file handlers
if frame != 0:
- raise EOFError
+ msg = "no more images in file"
+ raise EOFError(msg)
def show(self, title=None):
"""
@@ -2501,7 +2505,7 @@ class Image:
_show(self, title=title)
- def split(self):
+ def split(self) -> tuple[Image, ...]:
"""
Split this image into individual bands. This method returns a
tuple of individual image bands from an image. For example,
@@ -2544,7 +2548,7 @@ class Image:
return self._new(self.im.getband(channel))
- def tell(self):
+ def tell(self) -> int:
"""
Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`.
@@ -2651,7 +2655,7 @@ class Image:
resample=Resampling.NEAREST,
fill=1,
fillcolor=None,
- ):
+ ) -> Image:
"""
Transforms this image. This method creates a new image with the
given size, and the same mode as the original, and copies data
@@ -2674,6 +2678,10 @@ class Image:
def transform(self, size, data, resample, fill=1):
# Return result
+ Implementations of :py:class:`~PIL.Image.ImageTransformHandler`
+ for some of the :py:class:`Transform` methods are provided
+ in :py:mod:`~PIL.ImageTransform`.
+
It may also be an object with a ``method.getdata`` method
that returns a tuple supplying new ``method`` and ``data`` values::
@@ -2884,7 +2892,7 @@ class ImageTransformHandler:
def _wedge():
- """Create greyscale wedge (for debugging only)"""
+ """Create grayscale wedge (for debugging only)"""
return Image._init(core.wedge("L"))
@@ -2910,7 +2918,7 @@ def _check_size(size):
return True
-def new(mode, size, color=0):
+def new(mode, size, color=0) -> Image:
"""
Creates a new image with the given mode and size.
@@ -2952,7 +2960,7 @@ def new(mode, size, color=0):
return im
-def frombytes(mode, size, data, decoder_name="raw", *args):
+def frombytes(mode, size, data, decoder_name="raw", *args) -> Image:
"""
Creates a copy of an image memory from pixel data in a buffer.
@@ -2978,15 +2986,16 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
_check_size(size)
- # may pass tuple instead of argument list
- if len(args) == 1 and isinstance(args[0], tuple):
- args = args[0]
-
- if decoder_name == "raw" and args == ():
- args = mode
-
im = new(mode, size)
- im.frombytes(data, decoder_name, args)
+ if im.width != 0 and im.height != 0:
+ # may pass tuple instead of argument list
+ if len(args) == 1 and isinstance(args[0], tuple):
+ args = args[0]
+
+ if decoder_name == "raw" and args == ():
+ args = mode
+
+ im.frombytes(data, decoder_name, args)
return im
@@ -3107,7 +3116,8 @@ def fromarray(obj, mode=None):
try:
mode, rawmode = _fromarray_typemap[typekey]
except KeyError as e:
- msg = "Cannot handle this data type: %s, %s" % typekey
+ typekey_shape, typestr = typekey
+ msg = f"Cannot handle this data type: {typekey_shape}, {typestr}"
raise TypeError(msg) from e
else:
rawmode = mode
@@ -3199,7 +3209,7 @@ def _decompression_bomb_check(size):
)
-def open(fp, mode="r", formats=None):
+def open(fp, mode="r", formats=None) -> Image:
"""
Opens and identifies the given image file.
@@ -3424,7 +3434,7 @@ def merge(mode, bands):
# Plugin registry
-def register_open(id, factory, accept=None):
+def register_open(id, factory, accept=None) -> None:
"""
Register an image file plugin. This function should not be used
in application code.
@@ -3440,7 +3450,7 @@ def register_open(id, factory, accept=None):
OPEN[id] = factory, accept
-def register_mime(id, mimetype):
+def register_mime(id: str, mimetype: str) -> None:
"""
Registers an image MIME type by populating ``Image.MIME``. This function
should not be used in application code.
@@ -3455,7 +3465,7 @@ def register_mime(id, mimetype):
MIME[id.upper()] = mimetype
-def register_save(id, driver):
+def register_save(id: str, driver) -> None:
"""
Registers an image save function. This function should not be
used in application code.
@@ -3478,7 +3488,7 @@ def register_save_all(id, driver):
SAVE_ALL[id.upper()] = driver
-def register_extension(id, extension):
+def register_extension(id, extension) -> None:
"""
Registers an image extension. This function should not be
used in application code.
@@ -3489,7 +3499,7 @@ def register_extension(id, extension):
EXTENSION[extension.lower()] = id.upper()
-def register_extensions(id, extensions):
+def register_extensions(id, extensions) -> None:
"""
Registers image extensions. This function should not be
used in application code.
@@ -3510,7 +3520,7 @@ def registered_extensions():
return EXTENSION
-def register_decoder(name, decoder):
+def register_decoder(name: str, decoder) -> None:
"""
Registers an image decoder. This function should not be
used in application code.
diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py
index 701200317..29a5c995f 100644
--- a/src/PIL/ImageChops.py
+++ b/src/PIL/ImageChops.py
@@ -15,11 +15,13 @@
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
from . import Image
-def constant(image, value):
- """Fill a channel with a given grey level.
+def constant(image: Image.Image, value: int) -> Image.Image:
+ """Fill a channel with a given gray level.
:rtype: :py:class:`~PIL.Image.Image`
"""
@@ -27,7 +29,7 @@ def constant(image, value):
return Image.new("L", image.size, value)
-def duplicate(image):
+def duplicate(image: Image.Image) -> Image.Image:
"""Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`.
:rtype: :py:class:`~PIL.Image.Image`
@@ -36,7 +38,7 @@ def duplicate(image):
return image.copy()
-def invert(image):
+def invert(image: Image.Image) -> Image.Image:
"""
Invert an image (channel). ::
@@ -49,7 +51,7 @@ def invert(image):
return image._new(image.im.chop_invert())
-def lighter(image1, image2):
+def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Compares the two images, pixel by pixel, and returns a new image containing
the lighter values. ::
@@ -64,7 +66,7 @@ def lighter(image1, image2):
return image1._new(image1.im.chop_lighter(image2.im))
-def darker(image1, image2):
+def darker(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Compares the two images, pixel by pixel, and returns a new image containing
the darker values. ::
@@ -79,7 +81,7 @@ def darker(image1, image2):
return image1._new(image1.im.chop_darker(image2.im))
-def difference(image1, image2):
+def difference(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Returns the absolute value of the pixel-by-pixel difference between the two
images. ::
@@ -94,7 +96,7 @@ def difference(image1, image2):
return image1._new(image1.im.chop_difference(image2.im))
-def multiply(image1, image2):
+def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two images on top of each other.
@@ -111,7 +113,7 @@ def multiply(image1, image2):
return image1._new(image1.im.chop_multiply(image2.im))
-def screen(image1, image2):
+def screen(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two inverted images on top of each other. ::
@@ -125,7 +127,7 @@ def screen(image1, image2):
return image1._new(image1.im.chop_screen(image2.im))
-def soft_light(image1, image2):
+def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two images on top of each other using the Soft Light algorithm
@@ -137,7 +139,7 @@ def soft_light(image1, image2):
return image1._new(image1.im.chop_soft_light(image2.im))
-def hard_light(image1, image2):
+def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two images on top of each other using the Hard Light algorithm
@@ -149,7 +151,7 @@ def hard_light(image1, image2):
return image1._new(image1.im.chop_hard_light(image2.im))
-def overlay(image1, image2):
+def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""
Superimposes two images on top of each other using the Overlay algorithm
@@ -161,7 +163,9 @@ def overlay(image1, image2):
return image1._new(image1.im.chop_overlay(image2.im))
-def add(image1, image2, scale=1.0, offset=0):
+def add(
+ image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
+) -> Image.Image:
"""
Adds two images, dividing the result by scale and adding the
offset. If omitted, scale defaults to 1.0, and offset to 0.0. ::
@@ -176,7 +180,9 @@ def add(image1, image2, scale=1.0, offset=0):
return image1._new(image1.im.chop_add(image2.im, scale, offset))
-def subtract(image1, image2, scale=1.0, offset=0):
+def subtract(
+ image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
+) -> Image.Image:
"""
Subtracts two images, dividing the result by scale and adding the offset.
If omitted, scale defaults to 1.0, and offset to 0.0. ::
@@ -191,7 +197,7 @@ def subtract(image1, image2, scale=1.0, offset=0):
return image1._new(image1.im.chop_subtract(image2.im, scale, offset))
-def add_modulo(image1, image2):
+def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Add two images, without clipping the result. ::
out = ((image1 + image2) % MAX)
@@ -204,7 +210,7 @@ def add_modulo(image1, image2):
return image1._new(image1.im.chop_add_modulo(image2.im))
-def subtract_modulo(image1, image2):
+def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Subtract two images, without clipping the result. ::
out = ((image1 - image2) % MAX)
@@ -217,7 +223,7 @@ def subtract_modulo(image1, image2):
return image1._new(image1.im.chop_subtract_modulo(image2.im))
-def logical_and(image1, image2):
+def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Logical AND between two images.
Both of the images must have mode "1". If you would like to perform a
@@ -235,7 +241,7 @@ def logical_and(image1, image2):
return image1._new(image1.im.chop_and(image2.im))
-def logical_or(image1, image2):
+def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Logical OR between two images.
Both of the images must have mode "1". ::
@@ -250,7 +256,7 @@ def logical_or(image1, image2):
return image1._new(image1.im.chop_or(image2.im))
-def logical_xor(image1, image2):
+def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image:
"""Logical XOR between two images.
Both of the images must have mode "1". ::
@@ -265,7 +271,7 @@ def logical_xor(image1, image2):
return image1._new(image1.im.chop_xor(image2.im))
-def blend(image1, image2, alpha):
+def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image:
"""Blend images using constant transparency weight. Alias for
:py:func:`PIL.Image.blend`.
@@ -275,7 +281,9 @@ def blend(image1, image2, alpha):
return Image.blend(image1, image2, alpha)
-def composite(image1, image2, mask):
+def composite(
+ image1: Image.Image, image2: Image.Image, mask: Image.Image
+) -> Image.Image:
"""Create composite using transparency mask. Alias for
:py:func:`PIL.Image.composite`.
@@ -285,7 +293,7 @@ def composite(image1, image2, mask):
return Image.composite(image1, image2, mask)
-def offset(image, xoffset, yoffset=None):
+def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image:
"""Returns a copy of the image where data has been offset by the given
distances. Data wraps around the edges. If ``yoffset`` is omitted, it
is assumed to be equal to ``xoffset``.
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 3a337f9f2..3e40105e4 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -4,6 +4,9 @@
# Optional color management support, based on Kevin Cazabon's PyCMS
# library.
+# Originally released under LGPL. Graciously donated to PIL in
+# March 2009, for distribution under the standard PIL license
+
# History:
# 2009-03-08 fl Added to PIL.
@@ -14,11 +17,16 @@
# See the README file for information on usage and redistribution. See
# below for the original description.
+from __future__ import annotations
+import operator
import sys
-from enum import IntEnum
+from enum import IntEnum, IntFlag
+from functools import reduce
+from typing import Any
from . import Image
+from ._deprecate import deprecate
try:
from . import _imagingcms
@@ -27,9 +35,9 @@ except ImportError as ex:
# anything in core.
from ._util import DeferredError
- _imagingcms = DeferredError(ex)
+ _imagingcms = DeferredError.new(ex)
-DESCRIPTION = """
+_DESCRIPTION = """
pyCMS
a Python / PIL interface to the littleCMS ICC Color Management System
@@ -92,7 +100,22 @@ pyCMS
"""
-VERSION = "1.0.0 pil"
+_VERSION = "1.0.0 pil"
+
+
+def __getattr__(name: str) -> Any:
+ if name == "DESCRIPTION":
+ deprecate("PIL.ImageCms.DESCRIPTION", 12)
+ return _DESCRIPTION
+ elif name == "VERSION":
+ deprecate("PIL.ImageCms.VERSION", 12)
+ return _VERSION
+ elif name == "FLAGS":
+ deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags")
+ return _FLAGS
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
+
# --------------------------------------------------------------------.
@@ -118,7 +141,70 @@ class Direction(IntEnum):
#
# flags
-FLAGS = {
+
+class Flags(IntFlag):
+ """Flags and documentation are taken from ``lcms2.h``."""
+
+ NONE = 0
+ NOCACHE = 0x0040
+ """Inhibit 1-pixel cache"""
+ NOOPTIMIZE = 0x0100
+ """Inhibit optimizations"""
+ NULLTRANSFORM = 0x0200
+ """Don't transform anyway"""
+ GAMUTCHECK = 0x1000
+ """Out of Gamut alarm"""
+ SOFTPROOFING = 0x4000
+ """Do softproofing"""
+ BLACKPOINTCOMPENSATION = 0x2000
+ NOWHITEONWHITEFIXUP = 0x0004
+ """Don't fix scum dot"""
+ HIGHRESPRECALC = 0x0400
+ """Use more memory to give better accuracy"""
+ LOWRESPRECALC = 0x0800
+ """Use less memory to minimize resources"""
+ # this should be 8BITS_DEVICELINK, but that is not a valid name in Python:
+ USE_8BITS_DEVICELINK = 0x0008
+ """Create 8 bits devicelinks"""
+ GUESSDEVICECLASS = 0x0020
+ """Guess device class (for ``transform2devicelink``)"""
+ KEEP_SEQUENCE = 0x0080
+ """Keep profile sequence for devicelink creation"""
+ FORCE_CLUT = 0x0002
+ """Force CLUT optimization"""
+ CLUT_POST_LINEARIZATION = 0x0001
+ """create postlinearization tables if possible"""
+ CLUT_PRE_LINEARIZATION = 0x0010
+ """create prelinearization tables if possible"""
+ NONEGATIVES = 0x8000
+ """Prevent negative numbers in floating point transforms"""
+ COPY_ALPHA = 0x04000000
+ """Alpha channels are copied on ``cmsDoTransform()``"""
+ NODEFAULTRESOURCEDEF = 0x01000000
+
+ _GRIDPOINTS_1 = 1 << 16
+ _GRIDPOINTS_2 = 2 << 16
+ _GRIDPOINTS_4 = 4 << 16
+ _GRIDPOINTS_8 = 8 << 16
+ _GRIDPOINTS_16 = 16 << 16
+ _GRIDPOINTS_32 = 32 << 16
+ _GRIDPOINTS_64 = 64 << 16
+ _GRIDPOINTS_128 = 128 << 16
+
+ @staticmethod
+ def GRIDPOINTS(n: int) -> Flags:
+ """
+ Fine-tune control over number of gridpoints
+
+ :param n: :py:class:`int` in range ``0 <= n <= 255``
+ """
+ return Flags.NONE | ((n & 0xFF) << 16)
+
+
+_MAX_FLAG = reduce(operator.or_, Flags)
+
+
+_FLAGS = {
"MATRIXINPUT": 1,
"MATRIXOUTPUT": 2,
"MATRIXONLY": (1 | 2),
@@ -141,11 +227,6 @@ FLAGS = {
"GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints
}
-_MAX_FLAG = 0
-for flag in FLAGS.values():
- if isinstance(flag, int):
- _MAX_FLAG = _MAX_FLAG | flag
-
# --------------------------------------------------------------------.
# Experimental PIL-level API
@@ -217,7 +298,7 @@ class ImageCmsTransform(Image.ImagePointHandler):
intent=Intent.PERCEPTUAL,
proof=None,
proof_intent=Intent.ABSOLUTE_COLORIMETRIC,
- flags=0,
+ flags=Flags.NONE,
):
if proof is None:
self.transform = core.buildTransform(
@@ -302,7 +383,7 @@ def profileToProfile(
renderingIntent=Intent.PERCEPTUAL,
outputMode=None,
inPlace=False,
- flags=0,
+ flags=Flags.NONE,
):
"""
(pyCMS) Applies an ICC transformation to a given image, mapping from
@@ -419,7 +500,7 @@ def buildTransform(
inMode,
outMode,
renderingIntent=Intent.PERCEPTUAL,
- flags=0,
+ flags=Flags.NONE,
):
"""
(pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
@@ -481,7 +562,7 @@ def buildTransform(
raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- msg = "flags must be an integer between 0 and %s" + _MAX_FLAG
+ msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
raise PyCMSError(msg)
try:
@@ -504,7 +585,7 @@ def buildProofTransform(
outMode,
renderingIntent=Intent.PERCEPTUAL,
proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC,
- flags=FLAGS["SOFTPROOFING"],
+ flags=Flags.SOFTPROOFING,
):
"""
(pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
@@ -585,7 +666,7 @@ def buildProofTransform(
raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- msg = "flags must be an integer between 0 and %s" + _MAX_FLAG
+ msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
raise PyCMSError(msg)
try:
@@ -787,11 +868,8 @@ def getProfileInfo(profile):
# info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
description = profile.profile.profile_description
cpright = profile.profile.copyright
- arr = []
- for elt in (description, cpright):
- if elt:
- arr.append(elt)
- return "\r\n\r\n".join(arr) + "\r\n\r\n"
+ elements = [element for element in (description, cpright) if element]
+ return "\r\n\r\n".join(elements) + "\r\n\r\n"
except (AttributeError, OSError, TypeError, ValueError) as v:
raise PyCMSError(v) from v
@@ -1006,4 +1084,9 @@ def versions():
(pyCMS) Fetches versions.
"""
- return VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__
+ deprecate(
+ "PIL.ImageCms.versions()",
+ 12,
+ '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)',
+ )
+ return _VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__
diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py
index befc1fd1d..ad59b0667 100644
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -16,12 +16,15 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import re
+from functools import lru_cache
from . import Image
+@lru_cache
def getrgb(color):
"""
Convert a color string to an RGB or RGBA tuple. If the string cannot be
@@ -120,11 +123,12 @@ def getrgb(color):
raise ValueError(msg)
+@lru_cache
def getcolor(color, mode):
"""
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
- not color or a palette image, converts the RGB value to a greyscale value.
+ not color or a palette image, converts the RGB value to a grayscale value.
If the string cannot be parsed, this function raises a :py:exc:`ValueError`
exception.
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index fbf320d72..84665f54f 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -29,9 +29,11 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import math
import numbers
+import struct
from . import Image, ImageColor
@@ -542,7 +544,8 @@ class ImageDraw:
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
# extract mask and set text alpha
color, mask = mask, mask.getband(3)
- color.fillband(3, (ink >> 24) & 0xFF)
+ ink_alpha = struct.pack("i", ink)[3]
+ color.fillband(3, ink_alpha)
x, y = coord
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
else:
@@ -921,7 +924,7 @@ def floodfill(image, xy, value, border=None, thresh=0):
if border is None:
fill = _color_diff(p, background) <= thresh
else:
- fill = p != value and p != border
+ fill = p not in (value, border)
if fill:
pixel[s, t] = value
new_edge.add((s, t))
diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py
index 7ce0224a6..35ee5834e 100644
--- a/src/PIL/ImageDraw2.py
+++ b/src/PIL/ImageDraw2.py
@@ -22,7 +22,7 @@
.. seealso:: :py:mod:`PIL.ImageDraw`
"""
-
+from __future__ import annotations
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py
index 3b79d5c46..93a50d2a2 100644
--- a/src/PIL/ImageEnhance.py
+++ b/src/PIL/ImageEnhance.py
@@ -17,6 +17,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
from . import Image, ImageFilter, ImageStat
@@ -59,7 +60,7 @@ class Contrast(_Enhance):
This class can be used to control the contrast of an image, similar
to the contrast control on a TV set. An enhancement factor of 0.0
- gives a solid grey image. A factor of 1.0 gives the original image.
+ gives a solid gray image. A factor of 1.0 gives the original image.
"""
def __init__(self, image):
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 7dd33a4db..c7a7c6275 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -26,13 +26,16 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import io
import itertools
import struct
import sys
+from typing import Any, NamedTuple
from . import Image
+from ._deprecate import deprecate
from ._util import is_path
MAXBLOCK = 65536
@@ -61,15 +64,25 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`,
# Helpers
-def raise_oserror(error):
+def _get_oserror(error, *, encoder):
try:
msg = Image.core.getcodecstatus(error)
except AttributeError:
msg = ERRORS.get(error)
if not msg:
- msg = f"decoder error {error}"
- msg += " when reading image file"
- raise OSError(msg)
+ msg = f"{'encoder' if encoder else 'decoder'} error {error}"
+ msg += f" when {'writing' if encoder else 'reading'} image file"
+ return OSError(msg)
+
+
+def raise_oserror(error):
+ deprecate(
+ "raise_oserror",
+ 12,
+ action="It is only useful for translating error codes returned by a codec's "
+ "decode() method, which ImageFile already does automatically.",
+ )
+ raise _get_oserror(error, encoder=False)
def _tilesort(t):
@@ -77,6 +90,13 @@ def _tilesort(t):
return t[2]
+class _Tile(NamedTuple):
+ encoder_name: str
+ extents: tuple[int, int, int, int]
+ offset: int
+ args: tuple[Any, ...] | str | None
+
+
#
# --------------------------------------------------------------------
# ImageFile base class
@@ -187,6 +207,8 @@ class ImageFile(Image.Image):
if use_mmap:
# try memory mapping
decoder_name, extents, offset, args = self.tile[0]
+ if isinstance(args, str):
+ args = (args, 0, 1)
if (
decoder_name == "raw"
and len(args) >= 3
@@ -200,8 +222,8 @@ class ImageFile(Image.Image):
with open(self.filename) as fp:
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
if offset + self.size[1] * args[1] > self.map.size():
- # buffer is not large enough
- raise OSError
+ msg = "buffer is not large enough"
+ raise OSError(msg)
self.im = Image.core.map_buffer(
self.map, self.size, decoder_name, offset, args
)
@@ -285,7 +307,7 @@ class ImageFile(Image.Image):
if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0:
# still raised if decoder fails to return anything
- raise_oserror(err_code)
+ raise _get_oserror(err_code, encoder=False)
return Image.Image.load(self)
@@ -412,7 +434,7 @@ class Parser:
if e < 0:
# decoding error
self.image = None
- raise_oserror(e)
+ raise _get_oserror(e, encoder=False)
else:
# end of image
return
@@ -430,7 +452,6 @@ class Parser:
with io.BytesIO(self.data) as fp:
im = Image.open(fp)
except OSError:
- # traceback.print_exc()
pass # not enough data
else:
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
@@ -493,7 +514,7 @@ class Parser:
# --------------------------------------------------------------------
-def _save(im, fp, tile, bufsize=0):
+def _save(im, fp, tile, bufsize=0) -> None:
"""Helper to save image based on tile list
:param im: Image object.
@@ -521,13 +542,13 @@ def _save(im, fp, tile, bufsize=0):
fp.flush()
-def _encode_tile(im, fp, tile, bufsize, fh, exc=None):
- for e, b, o, a in tile:
- if o > 0:
- fp.seek(o)
- encoder = Image._getencoder(im.mode, e, a, im.encoderconfig)
+def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
+ for encoder_name, extents, offset, args in tile:
+ if offset > 0:
+ fp.seek(offset)
+ encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig)
try:
- encoder.setimage(im.im, b)
+ encoder.setimage(im.im, extents)
if encoder.pushes_fd:
encoder.setfd(fp)
errcode = encoder.encode_to_pyfd()[1]
@@ -543,8 +564,7 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None):
# slight speedup: compress to real file object
errcode = encoder.encode_to_file(fh, bufsize)
if errcode < 0:
- msg = f"encoder error {errcode} when writing image file"
- raise OSError(msg) from exc
+ raise _get_oserror(errcode, encoder=True) from exc
finally:
encoder.cleanup()
@@ -596,6 +616,8 @@ class PyCodecState:
class PyCodec:
+ fd: io.BytesIO | None
+
def __init__(self, mode, *args):
self.im = None
self.state = PyCodecState()
@@ -690,9 +712,10 @@ class PyDecoder(PyCodec):
If finished with decoding return -1 for the bytes consumed.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
- raise NotImplementedError()
+ msg = "unavailable in base decoder"
+ raise NotImplementedError(msg)
- def set_as_raw(self, data, rawmode=None):
+ def set_as_raw(self, data: bytes, rawmode=None) -> None:
"""
Convenience method to set the internal image from a stream of raw data
@@ -739,7 +762,8 @@ class PyEncoder(PyCodec):
If finished with encoding return 1 for the error code.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
- raise NotImplementedError()
+ msg = "unavailable in base encoder"
+ raise NotImplementedError(msg)
def encode_to_pyfd(self):
"""
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 57268b8f5..035b83c4d 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -14,6 +14,8 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
import functools
@@ -222,7 +224,7 @@ class UnsharpMask(MultibandFilter):
.. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking
- """ # noqa: E501
+ """
name = "UnsharpMask"
@@ -394,7 +396,7 @@ class Color3DLUT(MultibandFilter):
if hasattr(table, "shape"):
try:
import numpy
- except ImportError: # pragma: no cover
+ except ImportError:
pass
if numpy and isinstance(table, numpy.ndarray):
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index c29562135..a63b73b33 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -25,12 +25,16 @@
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
import base64
import os
import sys
import warnings
from enum import IntEnum
from io import BytesIO
+from pathlib import Path
+from typing import BinaryIO
from . import Image
from ._util import is_directory, is_path
@@ -49,7 +53,7 @@ try:
except ImportError as ex:
from ._util import DeferredError
- core = DeferredError(ex)
+ core = DeferredError.new(ex)
def _string_length_check(text):
@@ -145,6 +149,8 @@ class ImageFont:
:return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module.
"""
+ _string_length_check(text)
+ Image._decompression_bomb_check(self.font.getsize(text))
return self.font.getmask(text, mode)
def getbbox(self, text, *args, **kwargs):
@@ -185,9 +191,20 @@ class ImageFont:
class FreeTypeFont:
"""FreeType font wrapper (requires _imagingft service)"""
- def __init__(self, font=None, size=10, index=0, encoding="", layout_engine=None):
+ def __init__(
+ self,
+ font: bytes | str | Path | BinaryIO | None = None,
+ size: float = 10,
+ index: int = 0,
+ encoding: str = "",
+ layout_engine: Layout | None = None,
+ ) -> None:
# FIXME: use service provider instead
+ if size <= 0:
+ msg = "font size must be greater than 0"
+ raise ValueError(msg)
+
self.path = font
self.size = size
self.index = index
@@ -213,6 +230,8 @@ class FreeTypeFont:
)
if is_path(font):
+ if isinstance(font, Path):
+ font = str(font)
if sys.platform == "win32":
font_bytes_path = font if isinstance(font, bytes) else font.encode()
try:
@@ -375,8 +394,9 @@ class FreeTypeFont:
:param stroke_width: The width of the text stroke.
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
:return: ``(left, top, right, bottom)`` bounding box
"""
@@ -449,8 +469,9 @@ class FreeTypeFont:
.. versionadded:: 6.2.0
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
.. versionadded:: 8.0.0
@@ -541,8 +562,9 @@ class FreeTypeFont:
.. versionadded:: 6.2.0
:param anchor: The text anchor alignment. Determines the relative location of
- the anchor to the text. The default alignment is top left.
- See :ref:`text-anchors` for valid values.
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
.. versionadded:: 8.0.0
@@ -562,22 +584,13 @@ class FreeTypeFont:
_string_length_check(text)
if start is None:
start = (0, 0)
- im = None
- size = None
- def fill(mode, im_size):
- nonlocal im, size
+ def fill(width, height):
+ size = (width, height)
+ Image._decompression_bomb_check(size)
+ return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
- size = im_size
- if Image.MAX_IMAGE_PIXELS is not None:
- pixels = max(1, size[0]) * max(1, size[1])
- if pixels > 2 * Image.MAX_IMAGE_PIXELS:
- return
-
- im = Image.core.fill(mode, size)
- return im
-
- offset = self.font.render(
+ return self.font.render(
text,
fill,
mode,
@@ -590,8 +603,6 @@ class FreeTypeFont:
start[0],
start[1],
)
- Image._decompression_bomb_check(size)
- return im, offset
def font_variant(
self, font=None, size=None, index=None, encoding=None, layout_engine=None
@@ -712,7 +723,6 @@ class TransposedFont:
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
msg = "text length is undefined for text rotated by 90 or 270 degrees"
raise ValueError(msg)
- _string_length_check(text)
return self.font.getlength(text, *args, **kwargs)
@@ -775,7 +785,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
This specifies the character set to use. It does not alter the
encoding of any text provided in subsequent operations.
:param layout_engine: Which layout engine to use, if available:
- :data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`.
+ :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`.
If it is available, Raqm layout will be used by default.
Otherwise, basic layout will be used.
@@ -788,6 +798,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
.. versionadded:: 4.2.0
:return: A font object.
:exception OSError: If the file could not be read.
+ :exception ValueError: If the font size is not greater than zero.
"""
def freetype(font):
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index bcfffc3dc..a4993d3d4 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -14,6 +14,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import io
import os
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index eb6bbe6c6..a7652f237 100644
--- a/src/PIL/ImageMath.py
+++ b/src/PIL/ImageMath.py
@@ -14,23 +14,22 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import builtins
+from types import CodeType
+from typing import Any
from . import Image, _imagingmath
-def _isconstant(v):
- return isinstance(v, (int, float))
-
-
class _Operand:
"""Wraps an image operand, providing standard operators"""
- def __init__(self, im):
+ def __init__(self, im: Image.Image):
self.im = im
- def __fixup(self, im1):
+ def __fixup(self, im1: _Operand | float) -> Image.Image:
# convert image to suitable mode
if isinstance(im1, _Operand):
# argument was an image.
@@ -43,127 +42,136 @@ class _Operand:
raise ValueError(msg)
else:
# argument was a constant
- if _isconstant(im1) and self.im.mode in ("1", "L", "I"):
+ if isinstance(im1, (int, float)) and self.im.mode in ("1", "L", "I"):
return Image.new("I", self.im.size, im1)
else:
return Image.new("F", self.im.size, im1)
- def apply(self, op, im1, im2=None, mode=None):
- im1 = self.__fixup(im1)
+ def apply(
+ self,
+ op: str,
+ im1: _Operand | float,
+ im2: _Operand | float | None = None,
+ mode: str | None = None,
+ ) -> _Operand:
+ im_1 = self.__fixup(im1)
if im2 is None:
# unary operation
- out = Image.new(mode or im1.mode, im1.size, None)
- im1.load()
+ out = Image.new(mode or im_1.mode, im_1.size, None)
+ im_1.load()
try:
- op = getattr(_imagingmath, op + "_" + im1.mode)
+ op = getattr(_imagingmath, op + "_" + im_1.mode)
except AttributeError as e:
msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e
- _imagingmath.unop(op, out.im.id, im1.im.id)
+ _imagingmath.unop(op, out.im.id, im_1.im.id)
else:
# binary operation
- im2 = self.__fixup(im2)
- if im1.mode != im2.mode:
+ im_2 = self.__fixup(im2)
+ if im_1.mode != im_2.mode:
# convert both arguments to floating point
- if im1.mode != "F":
- im1 = im1.convert("F")
- if im2.mode != "F":
- im2 = im2.convert("F")
- if im1.size != im2.size:
+ if im_1.mode != "F":
+ im_1 = im_1.convert("F")
+ if im_2.mode != "F":
+ im_2 = im_2.convert("F")
+ if im_1.size != im_2.size:
# crop both arguments to a common size
- size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1]))
- if im1.size != size:
- im1 = im1.crop((0, 0) + size)
- if im2.size != size:
- im2 = im2.crop((0, 0) + size)
- out = Image.new(mode or im1.mode, im1.size, None)
- im1.load()
- im2.load()
+ size = (
+ min(im_1.size[0], im_2.size[0]),
+ min(im_1.size[1], im_2.size[1]),
+ )
+ if im_1.size != size:
+ im_1 = im_1.crop((0, 0) + size)
+ if im_2.size != size:
+ im_2 = im_2.crop((0, 0) + size)
+ out = Image.new(mode or im_1.mode, im_1.size, None)
+ im_1.load()
+ im_2.load()
try:
- op = getattr(_imagingmath, op + "_" + im1.mode)
+ op = getattr(_imagingmath, op + "_" + im_1.mode)
except AttributeError as e:
msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e
- _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id)
+ _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id)
return _Operand(out)
# unary operators
- def __bool__(self):
+ def __bool__(self) -> bool:
# an image is "true" if it contains at least one non-zero pixel
return self.im.getbbox() is not None
- def __abs__(self):
+ def __abs__(self) -> _Operand:
return self.apply("abs", self)
- def __pos__(self):
+ def __pos__(self) -> _Operand:
return self
- def __neg__(self):
+ def __neg__(self) -> _Operand:
return self.apply("neg", self)
# binary operators
- def __add__(self, other):
+ def __add__(self, other: _Operand | float) -> _Operand:
return self.apply("add", self, other)
- def __radd__(self, other):
+ def __radd__(self, other: _Operand | float) -> _Operand:
return self.apply("add", other, self)
- def __sub__(self, other):
+ def __sub__(self, other: _Operand | float) -> _Operand:
return self.apply("sub", self, other)
- def __rsub__(self, other):
+ def __rsub__(self, other: _Operand | float) -> _Operand:
return self.apply("sub", other, self)
- def __mul__(self, other):
+ def __mul__(self, other: _Operand | float) -> _Operand:
return self.apply("mul", self, other)
- def __rmul__(self, other):
+ def __rmul__(self, other: _Operand | float) -> _Operand:
return self.apply("mul", other, self)
- def __truediv__(self, other):
+ def __truediv__(self, other: _Operand | float) -> _Operand:
return self.apply("div", self, other)
- def __rtruediv__(self, other):
+ def __rtruediv__(self, other: _Operand | float) -> _Operand:
return self.apply("div", other, self)
- def __mod__(self, other):
+ def __mod__(self, other: _Operand | float) -> _Operand:
return self.apply("mod", self, other)
- def __rmod__(self, other):
+ def __rmod__(self, other: _Operand | float) -> _Operand:
return self.apply("mod", other, self)
- def __pow__(self, other):
+ def __pow__(self, other: _Operand | float) -> _Operand:
return self.apply("pow", self, other)
- def __rpow__(self, other):
+ def __rpow__(self, other: _Operand | float) -> _Operand:
return self.apply("pow", other, self)
# bitwise
- def __invert__(self):
+ def __invert__(self) -> _Operand:
return self.apply("invert", self)
- def __and__(self, other):
+ def __and__(self, other: _Operand | float) -> _Operand:
return self.apply("and", self, other)
- def __rand__(self, other):
+ def __rand__(self, other: _Operand | float) -> _Operand:
return self.apply("and", other, self)
- def __or__(self, other):
+ def __or__(self, other: _Operand | float) -> _Operand:
return self.apply("or", self, other)
- def __ror__(self, other):
+ def __ror__(self, other: _Operand | float) -> _Operand:
return self.apply("or", other, self)
- def __xor__(self, other):
+ def __xor__(self, other: _Operand | float) -> _Operand:
return self.apply("xor", self, other)
- def __rxor__(self, other):
+ def __rxor__(self, other: _Operand | float) -> _Operand:
return self.apply("xor", other, self)
- def __lshift__(self, other):
+ def __lshift__(self, other: _Operand | float) -> _Operand:
return self.apply("lshift", self, other)
- def __rshift__(self, other):
+ def __rshift__(self, other: _Operand | float) -> _Operand:
return self.apply("rshift", self, other)
# logical
@@ -173,56 +181,61 @@ class _Operand:
def __ne__(self, other):
return self.apply("ne", self, other)
- def __lt__(self, other):
+ def __lt__(self, other: _Operand | float) -> _Operand:
return self.apply("lt", self, other)
- def __le__(self, other):
+ def __le__(self, other: _Operand | float) -> _Operand:
return self.apply("le", self, other)
- def __gt__(self, other):
+ def __gt__(self, other: _Operand | float) -> _Operand:
return self.apply("gt", self, other)
- def __ge__(self, other):
+ def __ge__(self, other: _Operand | float) -> _Operand:
return self.apply("ge", self, other)
# conversions
-def imagemath_int(self):
+def imagemath_int(self: _Operand) -> _Operand:
return _Operand(self.im.convert("I"))
-def imagemath_float(self):
+def imagemath_float(self: _Operand) -> _Operand:
return _Operand(self.im.convert("F"))
# logical
-def imagemath_equal(self, other):
+def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("eq", self, other, mode="I")
-def imagemath_notequal(self, other):
+def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("ne", self, other, mode="I")
-def imagemath_min(self, other):
+def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("min", self, other)
-def imagemath_max(self, other):
+def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("max", self, other)
-def imagemath_convert(self, mode):
+def imagemath_convert(self: _Operand, mode: str) -> _Operand:
return _Operand(self.im.convert(mode))
-ops = {}
-for k, v in list(globals().items()):
- if k[:10] == "imagemath_":
- ops[k[10:]] = v
+ops = {
+ "int": imagemath_int,
+ "float": imagemath_float,
+ "equal": imagemath_equal,
+ "notequal": imagemath_notequal,
+ "min": imagemath_min,
+ "max": imagemath_max,
+ "convert": imagemath_convert,
+}
-def eval(expression, _dict={}, **kw):
+def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any:
"""
Evaluates an image expression.
@@ -236,16 +249,21 @@ def eval(expression, _dict={}, **kw):
"""
# build execution namespace
- args = ops.copy()
+ args: dict[str, Any] = ops.copy()
+ for k in list(_dict.keys()) + list(kw.keys()):
+ if "__" in k or hasattr(builtins, k):
+ msg = f"'{k}' not allowed"
+ raise ValueError(msg)
+
args.update(_dict)
args.update(kw)
- for k, v in list(args.items()):
+ for k, v in args.items():
if hasattr(v, "im"):
args[k] = _Operand(v)
compiled_code = compile(expression, "", "eval")
- def scan(code):
+ def scan(code: CodeType) -> None:
for const in code.co_consts:
if type(const) is type(compiled_code):
scan(const)
diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py
index a0b335142..0b31f6081 100644
--- a/src/PIL/ImageMode.py
+++ b/src/PIL/ImageMode.py
@@ -12,79 +12,85 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import sys
-
-# mode descriptor cache
-_modes = None
+from functools import lru_cache
class ModeDescriptor:
"""Wrapper for mode strings."""
- def __init__(self, mode, bands, basemode, basetype, typestr):
+ def __init__(
+ self,
+ mode: str,
+ bands: tuple[str, ...],
+ basemode: str,
+ basetype: str,
+ typestr: str,
+ ) -> None:
self.mode = mode
self.bands = bands
self.basemode = basemode
self.basetype = basetype
self.typestr = typestr
- def __str__(self):
+ def __str__(self) -> str:
return self.mode
-def getmode(mode):
+@lru_cache
+def getmode(mode: str) -> ModeDescriptor:
"""Gets a mode descriptor for the given mode."""
- global _modes
- if not _modes:
- # initialize mode cache
- modes = {}
- endian = "<" if sys.byteorder == "little" else ">"
- for m, (basemode, basetype, bands, typestr) in {
- # core modes
- # Bits need to be extended to bytes
- "1": ("L", "L", ("1",), "|b1"),
- "L": ("L", "L", ("L",), "|u1"),
- "I": ("L", "I", ("I",), endian + "i4"),
- "F": ("L", "F", ("F",), endian + "f4"),
- "P": ("P", "L", ("P",), "|u1"),
- "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"),
- "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"),
- "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"),
- "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"),
- "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"),
- # UNDONE - unsigned |u1i1i1
- "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"),
- "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"),
- # extra experimental modes
- "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"),
- "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"),
- "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"),
- "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"),
- "LA": ("L", "L", ("L", "A"), "|u1"),
- "La": ("L", "L", ("L", "a"), "|u1"),
- "PA": ("RGB", "L", ("P", "A"), "|u1"),
- }.items():
- modes[m] = ModeDescriptor(m, bands, basemode, basetype, typestr)
- # mapping modes
- for i16mode, typestr in {
- # I;16 == I;16L, and I;32 == I;32L
- "I;16": "u2",
- "I;16BS": ">i2",
- "I;16N": endian + "u2",
- "I;16NS": endian + "i2",
- "I;32": "u4",
- "I;32L": "i4",
- "I;32LS": ""
+
+ modes = {
+ # core modes
+ # Bits need to be extended to bytes
+ "1": ("L", "L", ("1",), "|b1"),
+ "L": ("L", "L", ("L",), "|u1"),
+ "I": ("L", "I", ("I",), endian + "i4"),
+ "F": ("L", "F", ("F",), endian + "f4"),
+ "P": ("P", "L", ("P",), "|u1"),
+ "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"),
+ "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"),
+ "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"),
+ "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"),
+ "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"),
+ # UNDONE - unsigned |u1i1i1
+ "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"),
+ "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"),
+ # extra experimental modes
+ "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"),
+ "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"),
+ "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"),
+ "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"),
+ "LA": ("L", "L", ("L", "A"), "|u1"),
+ "La": ("L", "L", ("L", "a"), "|u1"),
+ "PA": ("RGB", "L", ("P", "A"), "|u1"),
+ }
+ if mode in modes:
+ base_mode, base_type, bands, type_str = modes[mode]
+ return ModeDescriptor(mode, bands, base_mode, base_type, type_str)
+
+ mapping_modes = {
+ # I;16 == I;16L, and I;32 == I;32L
+ "I;16": "u2",
+ "I;16BS": ">i2",
+ "I;16N": endian + "u2",
+ "I;16NS": endian + "i2",
+ "I;32": "u4",
+ "I;32L": "i4",
+ "I;32LS": "
+from __future__ import annotations
import re
@@ -61,12 +62,14 @@ class LutBuilder:
"""
- def __init__(self, patterns=None, op_name=None):
+ def __init__(
+ self, patterns: list[str] | None = None, op_name: str | None = None
+ ) -> None:
if patterns is not None:
self.patterns = patterns
else:
self.patterns = []
- self.lut = None
+ self.lut: bytearray | None = None
if op_name is not None:
known_patterns = {
"corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"],
@@ -86,25 +89,27 @@ class LutBuilder:
self.patterns = known_patterns[op_name]
- def add_patterns(self, patterns):
+ def add_patterns(self, patterns: list[str]) -> None:
self.patterns += patterns
- def build_default_lut(self):
+ def build_default_lut(self) -> None:
symbols = [0, 1]
m = 1 << 4 # pos of current pixel
self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE))
- def get_lut(self):
+ def get_lut(self) -> bytearray | None:
return self.lut
- def _string_permute(self, pattern, permutation):
+ def _string_permute(self, pattern: str, permutation: list[int]) -> str:
"""string_permute takes a pattern and a permutation and returns the
string permuted according to the permutation list.
"""
assert len(permutation) == 9
return "".join(pattern[p] for p in permutation)
- def _pattern_permute(self, basic_pattern, options, basic_result):
+ def _pattern_permute(
+ self, basic_pattern: str, options: str, basic_result: int
+ ) -> list[tuple[str, int]]:
"""pattern_permute takes a basic pattern and its result and clones
the pattern according to the modifications described in the $options
parameter. It returns a list of all cloned patterns."""
@@ -134,12 +139,13 @@ class LutBuilder:
return patterns
- def build_lut(self):
+ def build_lut(self) -> bytearray:
"""Compile all patterns into a morphology lut.
TBD :Build based on (file) morphlut:modify_lut
"""
self.build_default_lut()
+ assert self.lut is not None
patterns = []
# Parse and create symmetries of the patterns strings
@@ -158,10 +164,10 @@ class LutBuilder:
patterns += self._pattern_permute(pattern, options, result)
# compile the patterns into regular expressions for speed
- for i, pattern in enumerate(patterns):
+ compiled_patterns = []
+ for pattern in patterns:
p = pattern[0].replace(".", "X").replace("X", "[01]")
- p = re.compile(p)
- patterns[i] = (p, pattern[1])
+ compiled_patterns.append((re.compile(p), pattern[1]))
# Step through table and find patterns that match.
# Note that all the patterns are searched. The last one
@@ -171,8 +177,8 @@ class LutBuilder:
bitpattern = bin(i)[2:]
bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1]
- for p, r in patterns:
- if p.match(bitpattern):
+ for pattern, r in compiled_patterns:
+ if pattern.match(bitpattern):
self.lut[i] = [0, 1][r]
return self.lut
@@ -181,7 +187,12 @@ class LutBuilder:
class MorphOp:
"""A class for binary morphological operators"""
- def __init__(self, lut=None, op_name=None, patterns=None):
+ def __init__(
+ self,
+ lut: bytearray | None = None,
+ op_name: str | None = None,
+ patterns: list[str] | None = None,
+ ) -> None:
"""Create a binary morphological operator"""
self.lut = lut
if op_name is not None:
@@ -189,7 +200,7 @@ class MorphOp:
elif patterns is not None:
self.lut = LutBuilder(patterns=patterns).build_lut()
- def apply(self, image):
+ def apply(self, image: Image.Image):
"""Run a single morphological operation on an image
Returns a tuple of the number of changed pixels and the
@@ -205,7 +216,7 @@ class MorphOp:
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage
- def match(self, image):
+ def match(self, image: Image.Image):
"""Get a list of coordinates matching the morphological operation on
an image.
@@ -220,7 +231,7 @@ class MorphOp:
raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id)
- def get_on_pixels(self, image):
+ def get_on_pixels(self, image: Image.Image):
"""Get a list of all turned on pixels in a binary image
Returns a list of tuples of (x,y) coordinates
@@ -231,7 +242,7 @@ class MorphOp:
raise ValueError(msg)
return _imagingmorph.get_on_pixels(image.im.id)
- def load_lut(self, filename):
+ def load_lut(self, filename: str) -> None:
"""Load an operator from an mrl file"""
with open(filename, "rb") as f:
self.lut = bytearray(f.read())
@@ -241,7 +252,7 @@ class MorphOp:
msg = "Wrong size operator file!"
raise Exception(msg)
- def save_lut(self, filename):
+ def save_lut(self, filename: str) -> None:
"""Save an operator to an mrl file"""
if self.lut is None:
msg = "No operator loaded"
@@ -249,6 +260,6 @@ class MorphOp:
with open(filename, "wb") as f:
f.write(self.lut)
- def set_lut(self, lut):
+ def set_lut(self, lut: bytearray | None) -> None:
"""Set the lut from an external source"""
self.lut = lut
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 42f2152b3..a9e626b2b 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -16,6 +16,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import functools
import operator
@@ -56,7 +57,7 @@ def _lut(image, lut):
lut = lut + lut + lut
return image.point(lut)
else:
- msg = "not supported for this image mode"
+ msg = f"not supported for mode {image.mode}"
raise OSError(msg)
@@ -557,9 +558,7 @@ def invert(image):
:param image: The image to invert.
:return: An image.
"""
- lut = []
- for i in range(256):
- lut.append(255 - i)
+ lut = list(range(255, -1, -1))
return image.point(lut) if image.mode == "1" else _lut(image, lut)
@@ -581,10 +580,8 @@ def posterize(image, bits):
:param bits: The number of bits to keep for each channel (1-8).
:return: An image.
"""
- lut = []
mask = ~(2 ** (8 - bits) - 1)
- for i in range(256):
- lut.append(i & mask)
+ lut = [i & mask for i in range(256)]
return _lut(image, lut)
@@ -593,7 +590,7 @@ def solarize(image, threshold=128):
Invert all pixel values above a threshold.
:param image: The image to solarize.
- :param threshold: All pixels above this greyscale level are inverted.
+ :param threshold: All pixels above this grayscale level are inverted.
:return: An image.
"""
lut = []
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index f0c094708..2b6cecc61 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -15,6 +15,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import array
@@ -102,6 +103,30 @@ class ImagePalette:
# Declare tostring as an alias for tobytes
tostring = tobytes
+ def _new_color_index(self, image=None, e=None):
+ if not isinstance(self.palette, bytearray):
+ self._palette = bytearray(self.palette)
+ index = len(self.palette) // 3
+ special_colors = ()
+ if image:
+ special_colors = (
+ image.info.get("background"),
+ image.info.get("transparency"),
+ )
+ while index in special_colors:
+ index += 1
+ if index >= 256:
+ if image:
+ # Search for an unused index
+ for i, count in reversed(list(enumerate(image.histogram()))):
+ if count == 0 and i not in special_colors:
+ index = i
+ break
+ if index >= 256:
+ msg = "cannot allocate more than 256 colors"
+ raise ValueError(msg) from e
+ return index
+
def getcolor(self, color, image=None):
"""Given an rgb tuple, allocate palette entry.
@@ -124,27 +149,7 @@ class ImagePalette:
return self.colors[color]
except KeyError as e:
# allocate new color slot
- if not isinstance(self.palette, bytearray):
- self._palette = bytearray(self.palette)
- index = len(self.palette) // 3
- special_colors = ()
- if image:
- special_colors = (
- image.info.get("background"),
- image.info.get("transparency"),
- )
- while index in special_colors:
- index += 1
- if index >= 256:
- if image:
- # Search for an unused index
- for i, count in reversed(list(enumerate(image.histogram()))):
- if count == 0 and i not in special_colors:
- index = i
- break
- if index >= 256:
- msg = "cannot allocate more than 256 colors"
- raise ValueError(msg) from e
+ index = self._new_color_index(image, e)
self.colors[color] = index
if index * 3 < len(self.palette):
self._palette = (
@@ -187,7 +192,7 @@ class ImagePalette:
# Internal
-def raw(rawmode, data):
+def raw(rawmode, data) -> ImagePalette:
palette = ImagePalette()
palette.rawmode = rawmode
palette.palette = data
@@ -200,20 +205,15 @@ def raw(rawmode, data):
def make_linear_lut(black, white):
- lut = []
if black == 0:
- for i in range(256):
- lut.append(white * i // 255)
- else:
- raise NotImplementedError # FIXME
- return lut
+ return [white * i // 255 for i in range(256)]
+
+ msg = "unavailable when black is non-zero"
+ raise NotImplementedError(msg) # FIXME
def make_gamma_lut(exp):
- lut = []
- for i in range(256):
- lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
- return lut
+ return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
def negative(mode="RGB"):
@@ -225,9 +225,7 @@ def negative(mode="RGB"):
def random(mode="RGB"):
from random import randint
- palette = []
- for i in range(256 * len(mode)):
- palette.append(randint(0, 255))
+ palette = [randint(0, 255) for _ in range(256 * len(mode))]
return ImagePalette(mode, palette)
@@ -256,8 +254,6 @@ def load(filename):
if lut:
break
except (SyntaxError, ValueError):
- # import traceback
- # traceback.print_exc()
pass
else:
msg = "cannot load palette"
diff --git a/src/PIL/ImagePath.py b/src/PIL/ImagePath.py
index 3d3538c97..77e8a609a 100644
--- a/src/PIL/ImagePath.py
+++ b/src/PIL/ImagePath.py
@@ -13,6 +13,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
from . import Image
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index 9b7245454..6377c7501 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -15,6 +15,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import sys
from io import BytesIO
@@ -83,16 +84,6 @@ def fromqimage(im):
def fromqpixmap(im):
return fromqimage(im)
- # buffer = QBuffer()
- # buffer.open(QIODevice.ReadWrite)
- # # im.save(buffer)
- # # What if png doesn't support some image features like animation?
- # im.save(buffer, 'ppm')
- # bytes_io = BytesIO()
- # bytes_io.write(buffer.data())
- # buffer.close()
- # bytes_io.seek(0)
- # return Image.open(bytes_io)
def align8to32(bytes, width, mode):
@@ -113,12 +104,10 @@ def align8to32(bytes, width, mode):
if not extra_padding:
return bytes
- new_data = []
- for i in range(len(bytes) // bytes_per_line):
- new_data.append(
- bytes[i * bytes_per_line : (i + 1) * bytes_per_line]
- + b"\x00" * extra_padding
- )
+ new_data = [
+ bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding
+ for i in range(len(bytes) // bytes_per_line)
+ ]
return b"".join(new_data)
@@ -141,15 +130,11 @@ def _toqclass_helper(im):
format = qt_format.Format_Mono
elif im.mode == "L":
format = qt_format.Format_Indexed8
- colortable = []
- for i in range(256):
- colortable.append(rgb(i, i, i))
+ colortable = [rgb(i, i, i) for i in range(256)]
elif im.mode == "P":
format = qt_format.Format_Indexed8
- colortable = []
palette = im.getpalette()
- for i in range(0, len(palette), 3):
- colortable.append(rgb(*palette[i : i + 3]))
+ colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)]
elif im.mode == "RGB":
# Populate the 4th channel with 255
im = im.convert("RGBA")
@@ -208,9 +193,5 @@ def toqimage(im):
def toqpixmap(im):
- # # This doesn't work. For now using a dumb approach.
- # im_data = _toqclass_helper(im)
- # result = QPixmap(im_data["size"][0], im_data["size"][1])
- # result.loadFromData(im_data["data"])
qimage = toqimage(im)
return QPixmap.fromImage(qimage)
diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py
index c4bb6334a..2c1850276 100644
--- a/src/PIL/ImageSequence.py
+++ b/src/PIL/ImageSequence.py
@@ -14,6 +14,11 @@
#
##
+from __future__ import annotations
+
+from typing import Callable
+
+from . import Image
class Iterator:
@@ -28,33 +33,38 @@ class Iterator:
:param im: An image object.
"""
- def __init__(self, im):
+ def __init__(self, im: Image.Image):
if not hasattr(im, "seek"):
msg = "im must have seek method"
raise AttributeError(msg)
self.im = im
self.position = getattr(self.im, "_min_frame", 0)
- def __getitem__(self, ix):
+ def __getitem__(self, ix: int) -> Image.Image:
try:
self.im.seek(ix)
return self.im
except EOFError as e:
- raise IndexError from e # end of sequence
+ msg = "end of sequence"
+ raise IndexError(msg) from e
- def __iter__(self):
+ def __iter__(self) -> Iterator:
return self
- def __next__(self):
+ def __next__(self) -> Image.Image:
try:
self.im.seek(self.position)
self.position += 1
return self.im
except EOFError as e:
- raise StopIteration from e
+ msg = "end of sequence"
+ raise StopIteration(msg) from e
-def all_frames(im, func=None):
+def all_frames(
+ im: Image.Image | list[Image.Image],
+ func: Callable[[Image.Image], Image.Image] | None = None,
+) -> list[Image.Image]:
"""
Applies a given function to all frames in an image or a list of images.
The frames are returned as a list of separate images.
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index 8b1c3f8bb..c03122c11 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -11,18 +11,22 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
+import abc
import os
import shutil
import subprocess
import sys
from shlex import quote
+from typing import Any
from . import Image
_viewers = []
-def register(viewer, order=1):
+def register(viewer, order: int = 1) -> None:
"""
The :py:func:`register` function is used to register additional viewers::
@@ -47,7 +51,7 @@ def register(viewer, order=1):
_viewers.insert(0, viewer)
-def show(image, title=None, **options):
+def show(image: Image.Image, title: str | None = None, **options: Any) -> bool:
r"""
Display a given image.
@@ -67,7 +71,7 @@ class Viewer:
# main api
- def show(self, image, **options):
+ def show(self, image: Image.Image, **options: Any) -> int:
"""
The main function for displaying an image.
Converts the given image to the target format and displays it.
@@ -85,31 +89,32 @@ class Viewer:
# hook methods
- format = None
+ format: str | None = None
"""The format to convert the image into."""
- options = {}
+ options: dict[str, Any] = {}
"""Additional options used to convert the image."""
- def get_format(self, image):
+ def get_format(self, image: Image.Image) -> str | None:
"""Return format name, or ``None`` to save as PGM/PPM."""
return self.format
- def get_command(self, file, **options):
+ def get_command(self, file: str, **options: Any) -> str:
"""
Returns the command used to display the file.
Not implemented in the base class.
"""
- raise NotImplementedError
+ msg = "unavailable in base viewer"
+ raise NotImplementedError(msg)
- def save_image(self, image):
+ def save_image(self, image: Image.Image) -> str:
"""Save to temporary file and return filename."""
return image._dump(format=self.get_format(image), **self.options)
- def show_image(self, image, **options):
+ def show_image(self, image: Image.Image, **options: Any) -> int:
"""Display the given image."""
return self.show_file(self.save_image(image), **options)
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -126,7 +131,7 @@ class WindowsViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
- def get_command(self, file, **options):
+ def get_command(self, file: str, **options: Any) -> str:
return (
f'start "Pillow" /WAIT "{file}" '
"&& ping -n 4 127.0.0.1 >NUL "
@@ -144,14 +149,14 @@ class MacViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
- def get_command(self, file, **options):
+ def get_command(self, file: str, **options: Any) -> str:
# on darwin open returns immediately resulting in the temp
# file removal while app is opening
command = "open -a Preview.app"
command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
return command
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -177,7 +182,11 @@ class UnixViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
- def get_command(self, file, **options):
+ @abc.abstractmethod
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
+ pass # pragma: no cover
+
+ def get_command(self, file: str, **options: Any) -> str:
command = self.get_command_ex(file, **options)[0]
return f"({command} {quote(file)}"
@@ -187,11 +196,11 @@ class XDGViewer(UnixViewer):
The freedesktop.org ``xdg-open`` command.
"""
- def get_command_ex(self, file, **options):
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
command = executable = "xdg-open"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -205,13 +214,15 @@ class DisplayViewer(UnixViewer):
This viewer supports the ``title`` parameter.
"""
- def get_command_ex(self, file, title=None, **options):
+ def get_command_ex(
+ self, file: str, title: str | None = None, **options: Any
+ ) -> tuple[str, str]:
command = executable = "display"
if title:
command += f" -title {quote(title)}"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -228,12 +239,12 @@ class DisplayViewer(UnixViewer):
class GmDisplayViewer(UnixViewer):
"""The GraphicsMagick ``gm display`` command."""
- def get_command_ex(self, file, **options):
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
executable = "gm"
command = "gm display"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -244,12 +255,12 @@ class GmDisplayViewer(UnixViewer):
class EogViewer(UnixViewer):
"""The GNOME Image Viewer ``eog`` command."""
- def get_command_ex(self, file, **options):
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
executable = "eog"
command = "eog -n"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -263,7 +274,9 @@ class XVViewer(UnixViewer):
This viewer supports the ``title`` parameter.
"""
- def get_command_ex(self, file, title=None, **options):
+ def get_command_ex(
+ self, file: str, title: str | None = None, **options: Any
+ ) -> tuple[str, str]:
# note: xv is pretty outdated. most modern systems have
# imagemagick's display command instead.
command = executable = "xv"
@@ -271,7 +284,7 @@ class XVViewer(UnixViewer):
command += f" -name {quote(title)}"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -301,7 +314,7 @@ if sys.platform not in ("win32", "darwin"): # unixoids
class IPythonViewer(Viewer):
"""The viewer for IPython frontends."""
- def show_image(self, image, **options):
+ def show_image(self, image: Image.Image, **options: Any) -> int:
ipython_display(image)
return 1
diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py
index b7ebddf06..13864e59c 100644
--- a/src/PIL/ImageStat.py
+++ b/src/PIL/ImageStat.py
@@ -20,10 +20,9 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
-import functools
import math
-import operator
class Stat:
@@ -53,26 +52,22 @@ class Stat:
"""Get min/max values for each band in the image"""
def minmax(histogram):
- n = 255
- x = 0
+ res_min, res_max = 255, 0
for i in range(256):
if histogram[i]:
- n = min(n, i)
- x = max(x, i)
- return n, x # returns (255, 0) if there's no data in the histogram
+ res_min = i
+ break
+ for i in range(255, -1, -1):
+ if histogram[i]:
+ res_max = i
+ break
+ return res_min, res_max
- v = []
- for i in range(0, len(self.h), 256):
- v.append(minmax(self.h[i:]))
- return v
+ return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)]
def _getcount(self):
"""Get total number of pixels in each layer"""
-
- v = []
- for i in range(0, len(self.h), 256):
- v.append(functools.reduce(operator.add, self.h[i : i + 256]))
- return v
+ return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)]
def _getsum(self):
"""Get sum of all pixels in each layer"""
@@ -98,11 +93,7 @@ class Stat:
def _getmean(self):
"""Get average pixel level for each layer"""
-
- v = []
- for i in self.bands:
- v.append(self.sum[i] / self.count[i])
- return v
+ return [self.sum[i] / self.count[i] for i in self.bands]
def _getmedian(self):
"""Get median pixel level for each layer"""
@@ -121,28 +112,18 @@ class Stat:
def _getrms(self):
"""Get RMS for each layer"""
-
- v = []
- for i in self.bands:
- v.append(math.sqrt(self.sum2[i] / self.count[i]))
- return v
+ return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
def _getvar(self):
"""Get variance for each layer"""
-
- v = []
- for i in self.bands:
- n = self.count[i]
- v.append((self.sum2[i] - (self.sum[i] ** 2.0) / n) / n)
- return v
+ return [
+ (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
+ for i in self.bands
+ ]
def _getstddev(self):
"""Get standard deviation for each layer"""
-
- v = []
- for i in self.bands:
- v.append(math.sqrt(self.var[i]))
- return v
+ return [math.sqrt(self.var[i]) for i in self.bands]
Global = Stat # compatibility
diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py
index bf98eb2c8..10b2cc69a 100644
--- a/src/PIL/ImageTk.py
+++ b/src/PIL/ImageTk.py
@@ -24,6 +24,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import tkinter
from io import BytesIO
diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py
index 7881f0d26..6aa82dadd 100644
--- a/src/PIL/ImageTransform.py
+++ b/src/PIL/ImageTransform.py
@@ -12,18 +12,31 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
+from typing import Sequence
from . import Image
class Transform(Image.ImageTransformHandler):
- def __init__(self, data):
+ """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`."""
+
+ method: Image.Transform
+
+ def __init__(self, data: Sequence[int]) -> None:
self.data = data
- def getdata(self):
+ def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
return self.method, self.data
- def transform(self, size, image, **options):
+ def transform(
+ self,
+ size: tuple[int, int],
+ image: Image.Image,
+ **options: dict[str, str | int | tuple[int, ...] | list[int]],
+ ) -> Image.Image:
+ """Perform the transform. Called from :py:meth:`.Image.transform`."""
# can be overridden
method, data = self.getdata()
return image.transform(size, method, data, **options)
@@ -41,7 +54,7 @@ class AffineTransform(Transform):
This function can be used to scale, translate, rotate, and shear the
original image.
- See :py:meth:`~PIL.Image.Image.transform`
+ See :py:meth:`.Image.transform`
:param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows
from an affine transform matrix.
@@ -50,6 +63,26 @@ class AffineTransform(Transform):
method = Image.Transform.AFFINE
+class PerspectiveTransform(Transform):
+ """
+ Define a perspective image transform.
+
+ This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel
+ (x, y) in the output image, the new value is taken from a position
+ ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in
+ the input image, rounded to nearest pixel.
+
+ This function can be used to scale, translate, rotate, and shear the
+ original image.
+
+ See :py:meth:`.Image.transform`
+
+ :param matrix: An 8-tuple (a, b, c, d, e, f, g, h).
+ """
+
+ method = Image.Transform.PERSPECTIVE
+
+
class ExtentTransform(Transform):
"""
Define a transform to extract a subregion from an image.
@@ -63,7 +96,7 @@ class ExtentTransform(Transform):
rectangle in the current image. It is slightly slower than crop, but about
as fast as a corresponding resize operation.
- See :py:meth:`~PIL.Image.Image.transform`
+ See :py:meth:`.Image.transform`
:param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the
input image's coordinate system. See :ref:`coordinate-system`.
@@ -79,7 +112,7 @@ class QuadTransform(Transform):
Maps a quadrilateral (a region defined by four corners) from the image to a
rectangle of the given size.
- See :py:meth:`~PIL.Image.Image.transform`
+ See :py:meth:`.Image.transform`
:param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the
upper left, lower left, lower right, and upper right corner of the
@@ -94,7 +127,7 @@ class MeshTransform(Transform):
Define a mesh image transform. A mesh transform consists of one or more
individual quad transforms.
- See :py:meth:`~PIL.Image.Image.transform`
+ See :py:meth:`.Image.transform`
:param data: A list of (bbox, quad) tuples.
"""
diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py
index ca9b14c8a..75910d2d9 100644
--- a/src/PIL/ImageWin.py
+++ b/src/PIL/ImageWin.py
@@ -16,6 +16,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
from . import Image
@@ -54,9 +55,9 @@ class Dib:
"L", "P", or "RGB".
If the display requires a palette, this constructor creates a suitable
- palette and associates it with the image. For an "L" image, 128 greylevels
+ palette and associates it with the image. For an "L" image, 128 graylevels
are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together
- with 20 greylevels.
+ with 20 graylevels.
To make sure that palettes work properly under Windows, you must call the
``palette`` method upon certain events from Windows.
diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py
index d409fcd59..abb3fb762 100644
--- a/src/PIL/ImtImagePlugin.py
+++ b/src/PIL/ImtImagePlugin.py
@@ -13,7 +13,7 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
import re
@@ -33,10 +33,12 @@ class ImtImageFile(ImageFile.ImageFile):
format = "IMT"
format_description = "IM Tools"
- def _open(self):
+ def _open(self) -> None:
# Quick rejection: if there's not a LF among the first
# 100 bytes, this is (probably) not a text header.
+ assert self.fp is not None
+
buffer = self.fp.read(100)
if b"\n" not in buffer:
msg = "not an IM file"
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index 316cd17c7..409609434 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -14,31 +14,50 @@
#
# See the README file for information on usage and redistribution.
#
-import os
-import tempfile
+from __future__ import annotations
+
+from io import BytesIO
+from typing import Sequence
from . import Image, ImageFile
-from ._binary import i8
from ._binary import i16be as i16
from ._binary import i32be as i32
-from ._binary import o8
+from ._deprecate import deprecate
COMPRESSION = {1: "raw", 5: "jpeg"}
-PAD = o8(0) * 4
+
+def __getattr__(name: str) -> bytes:
+ if name == "PAD":
+ deprecate("IptcImagePlugin.PAD", 12)
+ return b"\0\0\0\0"
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
#
# Helpers
-def i(c):
- return i32((PAD + c)[-4:])
+def _i(c: bytes) -> int:
+ return i32((b"\0\0\0\0" + c)[-4:])
-def dump(c):
+def _i8(c: int | bytes) -> int:
+ return c if isinstance(c, int) else c[0]
+
+
+def i(c: bytes) -> int:
+ """.. deprecated:: 10.2.0"""
+ deprecate("IptcImagePlugin.i", 12)
+ return _i(c)
+
+
+def dump(c: Sequence[int | bytes]) -> None:
+ """.. deprecated:: 10.2.0"""
+ deprecate("IptcImagePlugin.dump", 12)
for i in c:
- print("%02x" % i8(i), end=" ")
+ print("%02x" % _i8(i), end=" ")
print()
@@ -51,10 +70,10 @@ class IptcImageFile(ImageFile.ImageFile):
format = "IPTC"
format_description = "IPTC/NAA"
- def getint(self, key):
- return i(self.info[key])
+ def getint(self, key: tuple[int, int]) -> int:
+ return _i(self.info[key])
- def field(self):
+ def field(self) -> tuple[tuple[int, int] | None, int]:
#
# get a IPTC field header
s = self.fp.read(5)
@@ -76,13 +95,13 @@ class IptcImageFile(ImageFile.ImageFile):
elif size == 128:
size = 0
elif size > 128:
- size = i(self.fp.read(size - 128))
+ size = _i(self.fp.read(size - 128))
else:
size = i16(s, 3)
return tag, size
- def _open(self):
+ def _open(self) -> None:
# load descriptive fields
while True:
offset = self.fp.tell()
@@ -102,10 +121,10 @@ class IptcImageFile(ImageFile.ImageFile):
self.info[tag] = tagdata
# mode
- layers = i8(self.info[(3, 60)][0])
- component = i8(self.info[(3, 60)][1])
+ layers = self.info[(3, 60)][0]
+ component = self.info[(3, 60)][1]
if (3, 65) in self.info:
- id = i8(self.info[(3, 65)][0]) - 1
+ id = self.info[(3, 65)][0] - 1
else:
id = 0
if layers == 1 and not component:
@@ -127,27 +146,22 @@ class IptcImageFile(ImageFile.ImageFile):
# tile
if tag == (8, 10):
- self.tile = [
- ("iptc", (compression, offset), (0, 0, self.size[0], self.size[1]))
- ]
+ self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
def load(self):
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
return ImageFile.ImageFile.load(self)
- type, tile, box = self.tile[0]
-
- encoding, offset = tile
+ offset, compression = self.tile[0][2:]
self.fp.seek(offset)
# Copy image data to temporary file
- o_fd, outfile = tempfile.mkstemp(text=False)
- o = os.fdopen(o_fd)
- if encoding == "raw":
+ o = BytesIO()
+ if compression == "raw":
# To simplify access to the extracted file,
# prepend a PPM header
- o.write("P5\n%d %d\n255\n" % self.size)
+ o.write(b"P5\n%d %d\n255\n" % self.size)
while True:
type, size = self.field()
if type != (8, 10):
@@ -158,17 +172,10 @@ class IptcImageFile(ImageFile.ImageFile):
break
o.write(s)
size -= len(s)
- o.close()
- try:
- with Image.open(outfile) as _im:
- _im.load()
- self.im = _im.im
- finally:
- try:
- os.unlink(outfile)
- except OSError:
- pass
+ with Image.open(o) as _im:
+ _im.load()
+ self.im = _im.im
Image.register_open(IptcImageFile.format, IptcImageFile)
@@ -184,8 +191,6 @@ def getiptcinfo(im):
:returns: A dictionary containing IPTC information, or None if
no IPTC information block was found.
"""
- import io
-
from . import JpegImagePlugin, TiffImagePlugin
data = None
@@ -220,7 +225,7 @@ def getiptcinfo(im):
# parse the IPTC information chunk
im.info = {}
- im.fp = io.BytesIO(data)
+ im.fp = BytesIO(data)
try:
im._open()
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 963d6c1a3..4b778a0d3 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -13,6 +13,8 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
import io
import os
import struct
@@ -334,10 +336,7 @@ def _save(im, fp, filename):
if quality_layers is not None and not (
isinstance(quality_layers, (list, tuple))
and all(
- [
- isinstance(quality_layer, (int, float))
- for quality_layer in quality_layers
- ]
+ isinstance(quality_layer, (int, float)) for quality_layer in quality_layers
)
):
msg = "quality_layers must be a sequence of numbers"
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 917bbf39f..81b8749a3 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -31,6 +31,8 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
import array
import io
import math
@@ -85,10 +87,12 @@ def APP(self, marker):
self.info["dpi"] = jfif_density
self.info["jfif_unit"] = jfif_unit
self.info["jfif_density"] = jfif_density
- elif marker == 0xFFE1 and s[:5] == b"Exif\0":
- if "exif" not in self.info:
- # extract EXIF information (incomplete)
- self.info["exif"] = s # FIXME: value will change
+ elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
+ # extract EXIF information
+ if "exif" in self.info:
+ self.info["exif"] += s[6:]
+ else:
+ self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
@@ -165,7 +169,8 @@ def APP(self, marker):
except TypeError:
dpi = x_resolution
if math.isnan(dpi):
- raise ValueError
+ msg = "DPI is not a number"
+ raise ValueError(msg)
if resolution_unit == 3: # cm
# 1 dpcm = 2.54 dpi
dpi *= 2.54
@@ -232,9 +237,7 @@ def SOF(self, marker):
# fixup icc profile
self.icclist.sort() # sort by sequence number
if self.icclist[0][13] == len(self.icclist):
- profile = []
- for p in self.icclist:
- profile.append(p[14:])
+ profile = [p[14:] for p in self.icclist]
icc_profile = b"".join(profile)
else:
icc_profile = None # wrong number of fragments
@@ -396,7 +399,7 @@ class JpegImageFile(ImageFile.ImageFile):
# self.__offset = self.fp.tell()
break
s = self.fp.read(1)
- elif i == 0 or i == 0xFFFF:
+ elif i in {0, 0xFFFF}:
# padded marker or junk; move on
s = b"\xff"
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
@@ -719,7 +722,8 @@ def _save(im, fp, filename):
for idx, table in enumerate(qtables):
try:
if len(table) != 64:
- raise TypeError
+ msg = "Invalid quantization table"
+ raise TypeError(msg)
table = array.array("H", table)
except TypeError as e:
msg = "Invalid quantization table"
@@ -781,10 +785,13 @@ def _save(im, fp, filename):
progressive,
info.get("smooth", 0),
optimize,
+ info.get("keep_rgb", False),
info.get("streamtype", 0),
dpi[0],
dpi[1],
subsampling,
+ info.get("restart_marker_blocks", 0),
+ info.get("restart_marker_rows", 0),
qtables,
comment,
extra,
diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py
index a678e248e..9ecfdb259 100644
--- a/src/PIL/JpegPresets.py
+++ b/src/PIL/JpegPresets.py
@@ -62,6 +62,7 @@ Libjpeg ref.:
https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html
"""
+from __future__ import annotations
# fmt: off
presets = {
diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py
index bb79e71de..27972236c 100644
--- a/src/PIL/McIdasImagePlugin.py
+++ b/src/PIL/McIdasImagePlugin.py
@@ -15,14 +15,15 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import struct
from . import Image, ImageFile
-def _accept(s):
- return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04"
+def _accept(prefix: bytes) -> bool:
+ return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04"
##
@@ -33,8 +34,10 @@ class McIdasImageFile(ImageFile.ImageFile):
format = "MCIDAS"
format_description = "McIdas area file"
- def _open(self):
+ def _open(self) -> None:
# parse area file directory
+ assert self.fp is not None
+
s = self.fp.read(256)
if not _accept(s) or len(s) != 256:
msg = "not an McIdas area file"
diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py
index 801318930..f4529d9ae 100644
--- a/src/PIL/MicImagePlugin.py
+++ b/src/PIL/MicImagePlugin.py
@@ -15,7 +15,7 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
import olefile
@@ -51,10 +51,11 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
# find ACI subfiles with Image members (maybe not the
# best way to identify MIC files, but what the... ;-)
- self.images = []
- for path in self.ole.listdir():
- if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image":
- self.images.append(path)
+ self.images = [
+ path
+ for path in self.ole.listdir()
+ if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image"
+ ]
# if we didn't find any images, this is probably not
# an MIC file.
@@ -66,6 +67,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1
+ self.__fp = self.fp
self.seek(0)
def seek(self, frame):
@@ -87,10 +89,12 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
return self.frame
def close(self):
+ self.__fp.close()
self.ole.close()
super().close()
def __exit__(self, *args):
+ self.__fp.close()
self.ole.close()
super().__exit__()
diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py
index bfa88fe99..b9e9243e5 100644
--- a/src/PIL/MpegImagePlugin.py
+++ b/src/PIL/MpegImagePlugin.py
@@ -12,7 +12,9 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+from io import BytesIO
from . import Image, ImageFile
from ._binary import i8
@@ -22,15 +24,15 @@ from ._binary import i8
class BitStream:
- def __init__(self, fp):
+ def __init__(self, fp: BytesIO) -> None:
self.fp = fp
self.bits = 0
self.bitbuffer = 0
- def next(self):
+ def next(self) -> int:
return i8(self.fp.read(1))
- def peek(self, bits):
+ def peek(self, bits: int) -> int:
while self.bits < bits:
c = self.next()
if c < 0:
@@ -40,13 +42,13 @@ class BitStream:
self.bits += 8
return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1
- def skip(self, bits):
+ def skip(self, bits: int) -> None:
while self.bits < bits:
self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1))
self.bits += 8
self.bits = self.bits - bits
- def read(self, bits):
+ def read(self, bits: int) -> int:
v = self.peek(bits)
self.bits = self.bits - bits
return v
@@ -61,9 +63,10 @@ class MpegImageFile(ImageFile.ImageFile):
format = "MPEG"
format_description = "MPEG"
- def _open(self):
- s = BitStream(self.fp)
+ def _open(self) -> None:
+ assert self.fp is not None
+ s = BitStream(self.fp)
if s.read(32) != 0x1B3:
msg = "not an MPEG file"
raise SyntaxError(msg)
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index f9261c77d..199a10090 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -17,6 +17,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import itertools
import os
@@ -33,9 +34,6 @@ from . import (
from ._binary import i16be as i16
from ._binary import o32le
-# def _accept(prefix):
-# return JpegImagePlugin._accept(prefix)
-
def _save(im, fp, filename):
JpegImagePlugin._save(im, fp, filename)
diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py
index 3f3609f1c..bb7e466a7 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -22,6 +22,7 @@
# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
#
# See also: https://www.fileformat.info/format/mspaint/egff.htm
+from __future__ import annotations
import io
import struct
@@ -34,7 +35,7 @@ from ._binary import o16le as o16
# read MSP files
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:4] in [b"DanM", b"LinS"]
@@ -47,8 +48,10 @@ class MspImageFile(ImageFile.ImageFile):
format = "MSP"
format_description = "Windows Paint"
- def _open(self):
+ def _open(self) -> None:
# Header
+ assert self.fp is not None
+
s = self.fp.read(32)
if not _accept(s):
msg = "not an MSP file"
@@ -108,7 +111,9 @@ class MspDecoder(ImageFile.PyDecoder):
_pulls_fd = True
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
+ assert self.fd is not None
+
img = io.BytesIO()
blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8))
try:
@@ -158,7 +163,7 @@ Image.register_decoder("MSP", MspDecoder)
# write MSP files (uncompressed only)
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None:
if im.mode != "1":
msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg)
diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py
index 13b3048f6..848fc2f71 100644
--- a/src/PIL/PSDraw.py
+++ b/src/PIL/PSDraw.py
@@ -14,6 +14,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import sys
@@ -109,7 +110,7 @@ class PSDraw:
if im.mode == "1":
dpi = 200 # fax
else:
- dpi = 100 # greyscale
+ dpi = 100 # grayscale
# image size (on paper)
x = im.size[0] * 72 / dpi
y = im.size[1] * 72 / dpi
diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py
index 4a2c497fc..dc3175402 100644
--- a/src/PIL/PaletteFile.py
+++ b/src/PIL/PaletteFile.py
@@ -12,6 +12,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
from ._binary import o8
diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py
index a88a90791..65be7fef7 100644
--- a/src/PIL/PalmImagePlugin.py
+++ b/src/PIL/PalmImagePlugin.py
@@ -6,6 +6,7 @@
##
# Image plugin for Palm pixmap images (output only).
##
+from __future__ import annotations
from . import Image, ImageFile
from ._binary import o8
@@ -124,7 +125,7 @@ def _save(im, fp, filename):
if im.encoderinfo.get("bpp") in (1, 2, 4):
# this is 8-bit grayscale, so we shift it to get the high-order bits,
# and invert it because
- # Palm does greyscale from white (0) to black (1)
+ # Palm does grayscale from white (0) to black (1)
bpp = im.encoderinfo["bpp"]
im = im.point(
lambda x, shift=8 - bpp, maxval=(1 << bpp) - 1: maxval - (x >> shift)
diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py
index c7cbca8c5..1cd5c4a9d 100644
--- a/src/PIL/PcdImagePlugin.py
+++ b/src/PIL/PcdImagePlugin.py
@@ -13,7 +13,7 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
from . import Image, ImageFile
@@ -27,8 +27,10 @@ class PcdImageFile(ImageFile.ImageFile):
format = "PCD"
format_description = "Kodak PhotoCD"
- def _open(self):
+ def _open(self) -> None:
# rough
+ assert self.fp is not None
+
self.fp.seek(2048)
s = self.fp.read(2048)
@@ -47,9 +49,11 @@ class PcdImageFile(ImageFile.ImageFile):
self._size = 768, 512 # FIXME: not correct for rotated images!
self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)]
- def load_end(self):
+ def load_end(self) -> None:
if self.tile_post_rotate:
# Handle rotated PCDs
+ assert self.im is not None
+
self.im = self.im.rotate(self.tile_post_rotate)
self._size = self.im.size
diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py
index 8db5822fe..0d1968b14 100644
--- a/src/PIL/PcfFontFile.py
+++ b/src/PIL/PcfFontFile.py
@@ -15,8 +15,10 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import io
+from typing import BinaryIO, Callable
from . import FontFile, Image
from ._binary import i8
@@ -40,7 +42,7 @@ PCF_SWIDTHS = 1 << 6
PCF_GLYPH_NAMES = 1 << 7
PCF_BDF_ACCELERATORS = 1 << 8
-BYTES_PER_ROW = [
+BYTES_PER_ROW: list[Callable[[int], int]] = [
lambda bits: ((bits + 7) >> 3),
lambda bits: ((bits + 15) >> 3) & ~1,
lambda bits: ((bits + 31) >> 3) & ~3,
@@ -48,7 +50,7 @@ BYTES_PER_ROW = [
]
-def sz(s, o):
+def sz(s: bytes, o: int) -> bytes:
return s[o : s.index(b"\0", o)]
@@ -57,7 +59,7 @@ class PcfFontFile(FontFile.FontFile):
name = "name"
- def __init__(self, fp, charset_encoding="iso8859-1"):
+ def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"):
self.charset_encoding = charset_encoding
magic = l32(fp.read(4))
@@ -103,7 +105,9 @@ class PcfFontFile(FontFile.FontFile):
bitmaps[ix],
)
- def _getformat(self, tag):
+ def _getformat(
+ self, tag: int
+ ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]:
format, size, offset = self.toc[tag]
fp = self.fp
@@ -118,7 +122,7 @@ class PcfFontFile(FontFile.FontFile):
return fp, format, i16, i32
- def _load_properties(self):
+ def _load_properties(self) -> dict[bytes, bytes | int]:
#
# font properties
@@ -129,27 +133,24 @@ class PcfFontFile(FontFile.FontFile):
nprops = i32(fp.read(4))
# read property description
- p = []
- for i in range(nprops):
- p.append((i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))))
+ p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)]
+
if nprops & 3:
fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad
data = fp.read(i32(fp.read(4)))
for k, s, v in p:
- k = sz(data, k)
- if s:
- v = sz(data, v)
- properties[k] = v
+ property_value: bytes | int = sz(data, v) if s else v
+ properties[sz(data, k)] = property_value
return properties
- def _load_metrics(self):
+ def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]:
#
# font metrics
- metrics = []
+ metrics: list[tuple[int, int, int, int, int, int, int, int]] = []
fp, format, i16, i32 = self._getformat(PCF_METRICS)
@@ -182,12 +183,12 @@ class PcfFontFile(FontFile.FontFile):
return metrics
- def _load_bitmaps(self, metrics):
+ def _load_bitmaps(
+ self, metrics: list[tuple[int, int, int, int, int, int, int, int]]
+ ) -> list[Image.Image]:
#
# bitmap data
- bitmaps = []
-
fp, format, i16, i32 = self._getformat(PCF_BITMAPS)
nbitmaps = i32(fp.read(4))
@@ -196,13 +197,9 @@ class PcfFontFile(FontFile.FontFile):
msg = "Wrong number of bitmaps"
raise OSError(msg)
- offsets = []
- for i in range(nbitmaps):
- offsets.append(i32(fp.read(4)))
+ offsets = [i32(fp.read(4)) for _ in range(nbitmaps)]
- bitmap_sizes = []
- for i in range(4):
- bitmap_sizes.append(i32(fp.read(4)))
+ bitmap_sizes = [i32(fp.read(4)) for _ in range(4)]
# byteorder = format & 4 # non-zero => MSB
bitorder = format & 8 # non-zero => MSB
@@ -218,6 +215,7 @@ class PcfFontFile(FontFile.FontFile):
if bitorder:
mode = "1"
+ bitmaps = []
for i in range(nbitmaps):
xsize, ysize = metrics[i][:2]
b, e = offsets[i : i + 2]
@@ -227,7 +225,7 @@ class PcfFontFile(FontFile.FontFile):
return bitmaps
- def _load_encoding(self):
+ def _load_encoding(self) -> list[int | None]:
fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS)
first_col, last_col = i16(fp.read(2)), i16(fp.read(2))
@@ -238,7 +236,7 @@ class PcfFontFile(FontFile.FontFile):
nencoding = (last_col - first_col + 1) * (last_row - first_row + 1)
# map character code to bitmap index
- encoding = [None] * min(256, nencoding)
+ encoding: list[int | None] = [None] * min(256, nencoding)
encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)]
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 854d9e83e..3e0968a83 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -24,6 +24,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import io
import logging
@@ -36,7 +37,7 @@ from ._binary import o16le as o16
logger = logging.getLogger(__name__)
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
@@ -48,8 +49,10 @@ class PcxImageFile(ImageFile.ImageFile):
format = "PCX"
format_description = "Paintbrush"
- def _open(self):
+ def _open(self) -> None:
# header
+ assert self.fp is not None
+
s = self.fp.read(128)
if not _accept(s):
msg = "not a PCX file"
@@ -91,7 +94,7 @@ class PcxImageFile(ImageFile.ImageFile):
self.fp.seek(-769, io.SEEK_END)
s = self.fp.read(769)
if len(s) == 769 and s[0] == 12:
- # check if the palette is linear greyscale
+ # check if the palette is linear grayscale
for i in range(256):
if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3:
mode = rawmode = "P"
@@ -140,7 +143,7 @@ SAVE = {
}
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None:
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:
@@ -198,12 +201,14 @@ def _save(im, fp, filename):
if im.mode == "P":
# colour palette
+ assert im.im is not None
+
fp.write(o8(12))
palette = im.im.getpalette("RGB", "RGB")
palette += b"\x00" * (768 - len(palette))
fp.write(palette) # 768 bytes
elif im.mode == "L":
- # greyscale palette
+ # grayscale palette
fp.write(o8(12))
for i in range(256):
fp.write(o8(i) * 3)
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
index 09fc0c7e6..3506aadce 100644
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -19,6 +19,7 @@
##
# Image plugin for PDF images (output only).
##
+from __future__ import annotations
import io
import math
@@ -96,7 +97,7 @@ def _write_image(im, filename, existing_pdf, image_refs):
dict_obj["ColorSpace"] = [
PdfParser.PdfName("Indexed"),
PdfParser.PdfName("DeviceRGB"),
- 255,
+ len(palette) // 3 - 1,
PdfParser.PdfBinary(palette),
]
procset = "ImageI" # indexed color
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index dc1012f54..014460006 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import calendar
import codecs
import collections
@@ -82,7 +84,7 @@ class IndirectReference(
collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"])
):
def __str__(self):
- return "%s %s R" % self
+ return f"{self.object_id} {self.generation} R"
def __bytes__(self):
return self.__str__().encode("us-ascii")
@@ -103,7 +105,7 @@ class IndirectReference(
class IndirectObjectDef(IndirectReference):
def __str__(self):
- return "%s %s obj" % self
+ return f"{self.object_id} {self.generation} obj"
class XrefTable:
diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py
index 850272311..887b6568b 100644
--- a/src/PIL/PixarImagePlugin.py
+++ b/src/PIL/PixarImagePlugin.py
@@ -18,6 +18,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
from . import Image, ImageFile
from ._binary import i16le as i16
@@ -26,7 +27,7 @@ from ._binary import i16le as i16
# helpers
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\200\350\000\000"
@@ -38,8 +39,10 @@ class PixarImageFile(ImageFile.ImageFile):
format = "PIXAR"
format_description = "PIXAR raster image"
- def _open(self):
+ def _open(self) -> None:
# assuming a 4-byte magic label
+ assert self.fp is not None
+
s = self.fp.read(4)
if not _accept(s):
msg = "not a PIXAR file"
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 5e5a8cf6a..823f12492 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -30,6 +30,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import itertools
import logging
@@ -56,7 +57,7 @@ _MAGIC = b"\211PNG\r\n\032\n"
_MODES = {
# supported bits/color combinations, and corresponding modes/rawmodes
- # Greyscale
+ # Grayscale
(1, 0): ("1", "1"),
(2, 0): ("L", "L;2"),
(4, 0): ("L", "L;4"),
@@ -70,7 +71,7 @@ _MODES = {
(2, 3): ("P", "P;2"),
(4, 3): ("P", "P;4"),
(8, 3): ("P", "P"),
- # Greyscale with alpha
+ # Grayscale with alpha
(8, 4): ("LA", "LA"),
(16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available
# Truecolour with alpha
@@ -377,7 +378,7 @@ class PngStream(ChunkStream):
}
def rewind(self):
- self.im_info = self.rewind_state["info"]
+ self.im_info = self.rewind_state["info"].copy()
self.im_tile = self.rewind_state["tile"]
self._seq_num = self.rewind_state["seq_num"]
@@ -438,11 +439,12 @@ class PngStream(ChunkStream):
tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)]
self.im_tile = tile
self.im_idat = length
- raise EOFError
+ msg = "image data found"
+ raise EOFError(msg)
def chunk_IEND(self, pos, length):
- # end of PNG image
- raise EOFError
+ msg = "end of PNG image"
+ raise EOFError(msg)
def chunk_PLTE(self, pos, length):
# palette
@@ -891,7 +893,8 @@ class PngImageFile(ImageFile.ImageFile):
self.dispose_extent = self.info.get("bbox")
if not self.tile:
- raise EOFError
+ msg = "image not found in APNG frame"
+ raise EOFError(msg)
# setup frame disposal (actual disposal done when needed in the next _seek())
if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
@@ -1154,6 +1157,9 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
encoderinfo["duration"] = duration
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
+ if len(im_frames) == 1 and not default_image:
+ return im_frames[0]["im"]
+
# animation control
chunk(
fp,
@@ -1389,8 +1395,10 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, b"eXIf", exif)
if save_all:
- _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
- else:
+ im = _write_multiple_frames(
+ im, fp, chunk, rawmode, default_image, append_images
+ )
+ if im:
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
if info:
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index e480ab055..3e45ba95c 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -13,7 +13,10 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+import math
+from io import BytesIO
from . import Image, ImageFile
from ._binary import i16be as i16
@@ -35,6 +38,7 @@ MODES = {
b"P6": "RGB",
# extensions
b"P0CMYK": "CMYK",
+ b"Pf": "F",
# PIL extensions (for test purposes only)
b"PyP": "P",
b"PyRGBA": "RGBA",
@@ -42,8 +46,8 @@ MODES = {
}
-def _accept(prefix):
- return prefix[0:1] == b"P" and prefix[1] in b"0123456y"
+def _accept(prefix: bytes) -> bool:
+ return prefix[0:1] == b"P" and prefix[1] in b"0123456fy"
##
@@ -54,7 +58,9 @@ class PpmImageFile(ImageFile.ImageFile):
format = "PPM"
format_description = "Pbmplus image"
- def _read_magic(self):
+ def _read_magic(self) -> bytes:
+ assert self.fp is not None
+
magic = b""
# read until whitespace or longest available magic number
for _ in range(6):
@@ -64,7 +70,9 @@ class PpmImageFile(ImageFile.ImageFile):
magic += c
return magic
- def _read_token(self):
+ def _read_token(self) -> bytes:
+ assert self.fp is not None
+
token = b""
while len(token) <= 10: # read until next whitespace or limit of 10 characters
c = self.fp.read(1)
@@ -90,13 +98,16 @@ class PpmImageFile(ImageFile.ImageFile):
raise ValueError(msg)
return token
- def _open(self):
+ def _open(self) -> None:
+ assert self.fp is not None
+
magic_number = self._read_magic()
try:
mode = MODES[magic_number]
except KeyError:
msg = "not a PPM file"
raise SyntaxError(msg)
+ self._mode = mode
if magic_number in (b"P1", b"P4"):
self.custom_mimetype = "image/x-portable-bitmap"
@@ -105,40 +116,42 @@ class PpmImageFile(ImageFile.ImageFile):
elif magic_number in (b"P3", b"P6"):
self.custom_mimetype = "image/x-portable-pixmap"
- maxval = None
+ self._size = int(self._read_token()), int(self._read_token())
+
decoder_name = "raw"
if magic_number in (b"P1", b"P2", b"P3"):
decoder_name = "ppm_plain"
- for ix in range(3):
- token = int(self._read_token())
- if ix == 0: # token is the x size
- xsize = token
- elif ix == 1: # token is the y size
- ysize = token
- if mode == "1":
- self._mode = "1"
- rawmode = "1;I"
- break
- else:
- self._mode = rawmode = mode
- elif ix == 2: # token is maxval
- maxval = token
- if not 0 < maxval < 65536:
- msg = "maxval must be greater than 0 and less than 65536"
- raise ValueError(msg)
- if maxval > 255 and mode == "L":
- self._mode = "I"
- if decoder_name != "ppm_plain":
- # If maxval matches a bit depth, use the raw decoder directly
- if maxval == 65535 and mode == "L":
- rawmode = "I;16B"
- elif maxval != 255:
- decoder_name = "ppm"
+ args: str | tuple[str | int, ...]
+ if mode == "1":
+ args = "1;I"
+ elif mode == "F":
+ scale = float(self._read_token())
+ if scale == 0.0 or not math.isfinite(scale):
+ msg = "scale must be finite and non-zero"
+ raise ValueError(msg)
+ self.info["scale"] = abs(scale)
- args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
- self._size = xsize, ysize
- self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
+ rawmode = "F;32F" if scale < 0 else "F;32BF"
+ args = (rawmode, 0, -1)
+ else:
+ maxval = int(self._read_token())
+ if not 0 < maxval < 65536:
+ msg = "maxval must be greater than 0 and less than 65536"
+ raise ValueError(msg)
+ if maxval > 255 and mode == "L":
+ self._mode = "I"
+
+ rawmode = mode
+ if decoder_name != "ppm_plain":
+ # If maxval matches a bit depth, use the raw decoder directly
+ if maxval == 65535 and mode == "L":
+ rawmode = "I;16B"
+ elif maxval != 255:
+ decoder_name = "ppm"
+
+ args = rawmode if decoder_name == "raw" else (rawmode, maxval)
+ self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]
#
@@ -147,16 +160,19 @@ class PpmImageFile(ImageFile.ImageFile):
class PpmPlainDecoder(ImageFile.PyDecoder):
_pulls_fd = True
+ _comment_spans: bool
+
+ def _read_block(self) -> bytes:
+ assert self.fd is not None
- def _read_block(self):
return self.fd.read(ImageFile.SAFEBLOCK)
- def _find_comment_end(self, block, start=0):
+ def _find_comment_end(self, block: bytes, start: int = 0) -> int:
a = block.find(b"\n", start)
b = block.find(b"\r", start)
return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1)
- def _ignore_comments(self, block):
+ def _ignore_comments(self, block: bytes) -> bytes:
if self._comment_spans:
# Finish current comment
while block:
@@ -190,7 +206,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
break
return block
- def _decode_bitonal(self):
+ def _decode_bitonal(self) -> bytearray:
"""
This is a separate method because in the plain PBM format, all data tokens are
exactly one byte, so the inter-token whitespace is optional.
@@ -215,7 +231,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
invert = bytes.maketrans(b"01", b"\xFF\x00")
return data.translate(invert)
- def _decode_blocks(self, maxval):
+ def _decode_blocks(self, maxval: int) -> bytearray:
data = bytearray()
max_len = 10
out_byte_count = 4 if self.mode == "I" else 1
@@ -223,7 +239,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
bands = Image.getmodebands(self.mode)
total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count
- half_token = False
+ half_token = b""
while len(data) != total_bytes:
block = self._read_block() # read next block
if not block:
@@ -237,7 +253,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
if half_token:
block = half_token + block # stitch half_token to new block
- half_token = False
+ half_token = b""
tokens = block.split()
@@ -255,15 +271,15 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
raise ValueError(msg)
value = int(token)
if value > maxval:
- msg = f"Channel value too large for this mode: {value}"
- raise ValueError(msg)
+ msg_str = f"Channel value too large for this mode: {value}"
+ raise ValueError(msg_str)
value = round(value / maxval * out_max)
data += o32(value) if self.mode == "I" else o8(value)
if len(data) == total_bytes: # finished!
break
return data
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
self._comment_spans = False
if self.mode == "1":
data = self._decode_bitonal()
@@ -279,7 +295,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
class PpmDecoder(ImageFile.PyDecoder):
_pulls_fd = True
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
+ assert self.fd is not None
+
data = bytearray()
maxval = self.args[-1]
in_byte_count = 1 if maxval < 256 else 2
@@ -306,7 +324,7 @@ class PpmDecoder(ImageFile.PyDecoder):
# --------------------------------------------------------------------
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
if im.mode == "1":
rawmode, head = "1;I", b"P4"
elif im.mode == "L":
@@ -315,6 +333,8 @@ def _save(im, fp, filename):
rawmode, head = "I;16B", b"P5"
elif im.mode in ("RGB", "RGBA"):
rawmode, head = "RGB", b"P6"
+ elif im.mode == "F":
+ rawmode, head = "F;32F", b"Pf"
else:
msg = f"cannot write mode {im.mode} as PPM"
raise OSError(msg)
@@ -326,10 +346,10 @@ def _save(im, fp, filename):
fp.write(b"255\n")
else:
fp.write(b"65535\n")
- ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
-
- # ALTERNATIVE: save via builtin debug function
- # im._dump(filename)
+ elif head == b"Pf":
+ fp.write(b"-1.0\n")
+ row_order = -1 if im.mode == "F" else 1
+ ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))])
#
@@ -342,6 +362,6 @@ Image.register_save(PpmImageFile.format, _save)
Image.register_decoder("ppm", PpmDecoder)
Image.register_decoder("ppm_plain", PpmPlainDecoder)
-Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
+Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"])
Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index 2f019bb8c..5cff56413 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -15,6 +15,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import io
@@ -186,6 +187,9 @@ def _layerinfo(fp, ct_bytes):
ct_types = i16(read(2))
types = list(range(ct_types))
if len(types) > 4:
+ fp.seek(len(types) * 6 + 12, io.SEEK_CUR)
+ size = i32(read(4))
+ fp.seek(size, io.SEEK_CUR)
continue
for _ in types:
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
index 99b46a4a6..07bb712d8 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -18,6 +18,7 @@
# * Fill.c uses the integer form, but it's still going to use the old
# Access.c implementation.
#
+from __future__ import annotations
import logging
import sys
@@ -42,7 +43,7 @@ except ImportError as ex:
# anything in core.
from ._util import DeferredError
- FFI = ffi = DeferredError(ex)
+ FFI = ffi = DeferredError.new(ex)
logger = logging.getLogger(__name__)
@@ -244,7 +245,7 @@ class _PyAccessI16_L(PyAccess):
except TypeError:
color = min(color[0], 65535)
- pixel.l = color & 0xFF # noqa: E741
+ pixel.l = color & 0xFF
pixel.r = color >> 8
@@ -265,7 +266,7 @@ class _PyAccessI16_B(PyAccess):
except Exception:
color = min(color[0], 65535)
- pixel.l = color >> 8 # noqa: E741
+ pixel.l = color >> 8
pixel.r = color & 0xFF
diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py
index 66344faac..a7b9d4a9e 100644
--- a/src/PIL/QoiImagePlugin.py
+++ b/src/PIL/QoiImagePlugin.py
@@ -5,6 +5,7 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import os
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index acb9ce5a3..ccf661ff1 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -20,17 +20,18 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
import os
import struct
+from io import BytesIO
from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return len(prefix) >= 2 and i16(prefix) == 474
@@ -52,8 +53,10 @@ class SgiImageFile(ImageFile.ImageFile):
format = "SGI"
format_description = "SGI Image File Format"
- def _open(self):
+ def _open(self) -> None:
# HEAD
+ assert self.fp is not None
+
headlen = 512
s = self.fp.read(headlen)
@@ -122,8 +125,8 @@ class SgiImageFile(ImageFile.ImageFile):
]
-def _save(im, fp, filename):
- if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L":
+def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
+ if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode"
raise ValueError(msg)
@@ -155,7 +158,7 @@ def _save(im, fp, filename):
# Z Dimension: Number of channels
z = len(im.mode)
- if dim == 1 or dim == 2:
+ if dim in {1, 2}:
z = 1
# assert we've got the right number of bands.
@@ -168,8 +171,8 @@ def _save(im, fp, filename):
# Maximum Byte value (255 = 8bits per pixel)
pinmax = 255
# Image name (79 characters max, truncated below in write)
- img_name = os.path.splitext(os.path.basename(filename))[0]
- img_name = img_name.encode("ascii", "ignore")
+ filename = os.path.basename(filename)
+ img_name = os.path.splitext(filename)[0].encode("ascii", "ignore")
# Standard representation of pixel in the file
colormap = 0
fp.write(struct.pack(">h", magic_number))
@@ -201,7 +204,10 @@ def _save(im, fp, filename):
class SGI16Decoder(ImageFile.PyDecoder):
_pulls_fd = True
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
+ assert self.fd is not None
+ assert self.im is not None
+
rawmode, stride, orientation = self.args
pagesize = self.state.xsize * self.state.ysize
zsize = len(self.mode)
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 408b982b5..86582fb12 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -32,6 +32,8 @@
# Details about the Spider image format:
# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html
#
+from __future__ import annotations
+
import os
import struct
import sys
@@ -238,9 +240,7 @@ def makeSpiderHeader(im):
if nvalues < 23:
return []
- hdr = []
- for i in range(nvalues):
- hdr.append(0.0)
+ hdr = [0.0] * nvalues
# NB these are Fortran indices
hdr[1] = 1.0 # nslice (=1 for an image)
diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py
index 6a8d5d86b..4e098474a 100644
--- a/src/PIL/SunImagePlugin.py
+++ b/src/PIL/SunImagePlugin.py
@@ -15,13 +15,13 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
from . import Image, ImageFile, ImagePalette
from ._binary import i32be as i32
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return len(prefix) >= 4 and i32(prefix) == 0x59A66A95
@@ -33,7 +33,7 @@ class SunImageFile(ImageFile.ImageFile):
format = "SUN"
format_description = "Sun Raster File"
- def _open(self):
+ def _open(self) -> None:
# The Sun Raster file header is 32 bytes in length
# and has the following format:
@@ -49,6 +49,8 @@ class SunImageFile(ImageFile.ImageFile):
# DWORD ColorMapLength; /* Size of the color map in bytes */
# } SUNRASTER;
+ assert self.fp is not None
+
# HEAD
s = self.fp.read(32)
if not _accept(s):
diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py
index 32928f6af..7470663b4 100644
--- a/src/PIL/TarIO.py
+++ b/src/PIL/TarIO.py
@@ -13,16 +13,18 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import io
+from types import TracebackType
from . import ContainerIO
-class TarIO(ContainerIO.ContainerIO):
+class TarIO(ContainerIO.ContainerIO[bytes]):
"""A file object that provides read access to a given member of a TAR file."""
- def __init__(self, tarfile, file):
+ def __init__(self, tarfile: str, file: str) -> None:
"""
Create file object.
@@ -56,11 +58,16 @@ class TarIO(ContainerIO.ContainerIO):
super().__init__(self.fh, self.fh.tell(), size)
# Context manager support
- def __enter__(self):
+ def __enter__(self) -> TarIO:
return self
- def __exit__(self, *args):
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
self.close()
- def close(self):
+ def close(self) -> None:
self.fh.close()
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index f24ee4f5c..584932d2c 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -15,9 +15,10 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
import warnings
+from io import BytesIO
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@@ -49,8 +50,10 @@ class TgaImageFile(ImageFile.ImageFile):
format = "TGA"
format_description = "Targa"
- def _open(self):
+ def _open(self) -> None:
# process header
+ assert self.fp is not None
+
s = self.fp.read(18)
id_len = s[0]
@@ -151,8 +154,9 @@ class TgaImageFile(ImageFile.ImageFile):
except KeyError:
pass # cannot decode
- def load_end(self):
+ def load_end(self) -> None:
if self._flip_horizontally:
+ assert self.im is not None
self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
@@ -171,7 +175,7 @@ SAVE = {
}
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e:
@@ -194,6 +198,7 @@ def _save(im, fp, filename):
warnings.warn("id_section has been trimmed to 255 characters")
if colormaptype:
+ assert im.im is not None
palette = im.im.getpalette("RGB", "BGR")
colormaplength, colormapentry = len(palette) // 3, 24
else:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index dabf8dbfb..e20d4d5ea 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -38,6 +38,8 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
+
import io
import itertools
import logging
@@ -1702,25 +1704,27 @@ def _save(im, fp, filename):
colormap += [0] * (256 - colors)
ifd[COLORMAP] = colormap
# data orientation
- stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
- # aim for given strip size (64 KB by default) when using libtiff writer
- if libtiff:
- im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
- rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
- # JPEG encoder expects multiple of 8 rows
- if compression == "jpeg":
- rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
- else:
- rows_per_strip = im.size[1]
- if rows_per_strip == 0:
- rows_per_strip = 1
- strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
- strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
- ifd[ROWSPERSTRIP] = rows_per_strip
+ w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
+ stride = len(bits) * ((w * bits[0] + 7) // 8)
+ if ROWSPERSTRIP not in ifd:
+ # aim for given strip size (64 KB by default) when using libtiff writer
+ if libtiff:
+ im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
+ rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
+ # JPEG encoder expects multiple of 8 rows
+ if compression == "jpeg":
+ rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
+ else:
+ rows_per_strip = h
+ if rows_per_strip == 0:
+ rows_per_strip = 1
+ ifd[ROWSPERSTRIP] = rows_per_strip
+ strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP]
+ strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
if strip_byte_counts >= 2**16:
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
- stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),
+ stride * h - strip_byte_counts * (strips_per_image - 1),
)
ifd[STRIPOFFSETS] = tuple(
range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
@@ -1885,13 +1889,14 @@ class AppendingTiffWriter:
8, # long8
]
- # StripOffsets = 273
- # FreeOffsets = 288
- # TileOffsets = 324
- # JPEGQTables = 519
- # JPEGDCTables = 520
- # JPEGACTables = 521
- Tags = {273, 288, 324, 519, 520, 521}
+ Tags = {
+ 273, # StripOffsets
+ 288, # FreeOffsets
+ 324, # TileOffsets
+ 519, # JPEGQTables
+ 520, # JPEGDCTables
+ 521, # JPEGACTables
+ }
def __init__(self, fn, new=False):
if hasattr(fn, "read"):
@@ -1941,8 +1946,6 @@ class AppendingTiffWriter:
iimm = self.f.read(4)
if not iimm:
- # msg = "nothing written into new page"
- # raise RuntimeError(msg)
# Make it easy to finish a frame without committing to a new one.
return
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 30b05e4e1..88ff2f4fc 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -16,6 +16,7 @@
# This module provides constants and clear-text names for various
# well-known TIFF tags.
##
+from __future__ import annotations
from collections import namedtuple
@@ -56,7 +57,7 @@ def lookup(tag, group=None):
##
# Map tag numbers to tag info.
#
-# id: (Name, Type, Length, enum_values)
+# id: (Name, Type, Length[, enum_values])
#
# The length here differs from the length in the tiff spec. For
# numbers, the tiff spec is for the number of fields returned. We
@@ -427,7 +428,7 @@ def _populate():
TAGS_V2[k] = TagInfo(k, *v)
- for group, tags in TAGS_V2_GROUPS.items():
+ for tags in TAGS_V2_GROUPS.values():
for k, v in tags.items():
tags[k] = TagInfo(k, *v)
@@ -438,22 +439,6 @@ _populate()
TYPES = {}
-# was:
-# TYPES = {
-# 1: "byte",
-# 2: "ascii",
-# 3: "short",
-# 4: "long",
-# 5: "rational",
-# 6: "signed byte",
-# 7: "undefined",
-# 8: "signed short",
-# 9: "signed long",
-# 10: "signed rational",
-# 11: "float",
-# 12: "double",
-# }
-
#
# These tags are handled by default in libtiff, without
# adding to the custom dictionary. From tif_dir.c, searching for
diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py
index 3d9f97f84..c5bf3e04c 100644
--- a/src/PIL/WalImageFile.py
+++ b/src/PIL/WalImageFile.py
@@ -22,6 +22,7 @@ and has been tested with a few sample files found using google.
is not registered for use with :py:func:`PIL.Image.open()`.
To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead.
"""
+from __future__ import annotations
from . import Image, ImageFile
from ._binary import i32le as i32
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index 612fc0946..59556206a 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from io import BytesIO
from . import Image, ImageFile
@@ -168,6 +170,9 @@ class WebPImageFile(ImageFile.ImageFile):
return super().load()
+ def load_seek(self, pos):
+ pass
+
def tell(self):
if not _webp.HAVE_WEBPANIM:
return super().tell()
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index 3e5fb0151..b5b8c69b1 100644
--- a/src/PIL/WmfImagePlugin.py
+++ b/src/PIL/WmfImagePlugin.py
@@ -18,6 +18,7 @@
# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-WMF/[MS-WMF].pdf
# http://wvware.sourceforge.net/caolan/index.html
# http://wvware.sourceforge.net/caolan/ora-wmf.html
+from __future__ import annotations
from . import Image, ImageFile
from ._binary import i16le as word
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index eda60c5c5..c84adaca2 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -16,6 +16,7 @@
# To do:
# FIXME: make save work (this requires quantization support)
#
+from __future__ import annotations
from . import Image, ImageFile, ImagePalette
from ._binary import o8
@@ -32,7 +33,7 @@ for r in range(8):
)
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:6] == _MAGIC
@@ -44,8 +45,10 @@ class XVThumbImageFile(ImageFile.ImageFile):
format = "XVThumb"
format_description = "XV thumbnail image"
- def _open(self):
+ def _open(self) -> None:
# check magic
+ assert self.fp is not None
+
if not _accept(self.fp.read(6)):
msg = "not an XV thumbnail file"
raise SyntaxError(msg)
diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py
index 71cd57d74..0291e2858 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -18,8 +18,10 @@
#
# See the README file for information on usage and redistribution.
#
+from __future__ import annotations
import re
+from io import BytesIO
from . import Image, ImageFile
@@ -35,7 +37,7 @@ xbm_head = re.compile(
)
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix.lstrip()[:7] == b"#define"
@@ -47,7 +49,9 @@ class XbmImageFile(ImageFile.ImageFile):
format = "XBM"
format_description = "X11 Bitmap"
- def _open(self):
+ def _open(self) -> None:
+ assert self.fp is not None
+
m = xbm_head.match(self.fp.read(512))
if not m:
@@ -66,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile):
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)]
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
if im.mode != "1":
msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg)
diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py
index 8491d3b7e..bf73c9bef 100644
--- a/src/PIL/XpmImagePlugin.py
+++ b/src/PIL/XpmImagePlugin.py
@@ -13,7 +13,7 @@
#
# See the README file for information on usage and redistribution.
#
-
+from __future__ import annotations
import re
diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py
index 2bb8f6d7f..3fcac8643 100644
--- a/src/PIL/__init__.py
+++ b/src/PIL/__init__.py
@@ -12,6 +12,7 @@ Use PIL.__version__ for this Pillow version.
;-)
"""
+from __future__ import annotations
from . import _version
diff --git a/src/PIL/__main__.py b/src/PIL/__main__.py
index a05323f93..943789923 100644
--- a/src/PIL/__main__.py
+++ b/src/PIL/__main__.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from .features import pilinfo
pilinfo()
diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py
index a74ee9eb6..0a07e8d0e 100644
--- a/src/PIL/_binary.py
+++ b/src/PIL/_binary.py
@@ -13,21 +13,21 @@
"""Binary input/output support routines."""
-
+from __future__ import annotations
from struct import pack, unpack_from
-def i8(c):
- return c if c.__class__ is int else c[0]
+def i8(c: bytes) -> int:
+ return c[0]
-def o8(i):
+def o8(i: int) -> bytes:
return bytes((i & 255,))
# Input, le = little endian, be = big endian
-def i16le(c, o=0):
+def i16le(c: bytes, o: int = 0) -> int:
"""
Converts a 2-bytes (16 bits) string to an unsigned integer.
@@ -37,7 +37,7 @@ def i16le(c, o=0):
return unpack_from(" int:
"""
Converts a 2-bytes (16 bits) string to a signed integer.
@@ -47,7 +47,7 @@ def si16le(c, o=0):
return unpack_from(" int:
"""
Converts a 2-bytes (16 bits) string to a signed integer, big endian.
@@ -57,7 +57,7 @@ def si16be(c, o=0):
return unpack_from(">h", c, o)[0]
-def i32le(c, o=0):
+def i32le(c: bytes, o: int = 0) -> int:
"""
Converts a 4-bytes (32 bits) string to an unsigned integer.
@@ -67,7 +67,7 @@ def i32le(c, o=0):
return unpack_from(" int:
"""
Converts a 4-bytes (32 bits) string to a signed integer.
@@ -77,26 +77,26 @@ def si32le(c, o=0):
return unpack_from(" int:
return unpack_from(">H", c, o)[0]
-def i32be(c, o=0):
+def i32be(c: bytes, o: int = 0) -> int:
return unpack_from(">I", c, o)[0]
# Output, le = little endian, be = big endian
-def o16le(i):
+def o16le(i: int) -> bytes:
return pack(" bytes:
return pack(" bytes:
return pack(">H", i)
-def o32be(i):
+def o32be(i: int) -> bytes:
return pack(">I", i)
diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py
index 2f2a3df13..33a0e07b3 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -47,6 +47,8 @@ def deprecate(
raise RuntimeError(msg)
elif when == 11:
removed = "Pillow 11 (2024-10-15)"
+ elif when == 12:
+ removed = "Pillow 12 (2025-10-15)"
else:
msg = f"Unknown removal version: {when}. Update {__name__}?"
raise ValueError(msg)
diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi
new file mode 100644
index 000000000..b0235555d
--- /dev/null
+++ b/src/PIL/_imagingcms.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi
new file mode 100644
index 000000000..b0235555d
--- /dev/null
+++ b/src/PIL/_imagingft.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_imagingmath.pyi b/src/PIL/_imagingmath.pyi
new file mode 100644
index 000000000..b0235555d
--- /dev/null
+++ b/src/PIL/_imagingmath.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_imagingmorph.pyi b/src/PIL/_imagingmorph.pyi
new file mode 100644
index 000000000..b0235555d
--- /dev/null
+++ b/src/PIL/_imagingmorph.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py
index 597c21b5e..03a6eba44 100644
--- a/src/PIL/_tkinter_finder.py
+++ b/src/PIL/_tkinter_finder.py
@@ -1,5 +1,7 @@
""" Find compiled module linking to Tcl / Tk libraries
"""
+from __future__ import annotations
+
import sys
import tkinter
from tkinter import _tkinter as tk
diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py
new file mode 100644
index 000000000..608b2b41f
--- /dev/null
+++ b/src/PIL/_typing.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import sys
+
+if sys.version_info >= (3, 10):
+ from typing import TypeGuard
+else:
+ try:
+ from typing_extensions import TypeGuard
+ except ImportError:
+ from typing import Any
+
+ class TypeGuard: # type: ignore[no-redef]
+ def __class_getitem__(cls, item: Any) -> type[bool]:
+ return bool
+
+
+__all__ = ["TypeGuard"]
diff --git a/src/PIL/_util.py b/src/PIL/_util.py
index ba27b7e49..13f369cca 100644
--- a/src/PIL/_util.py
+++ b/src/PIL/_util.py
@@ -1,19 +1,32 @@
+from __future__ import annotations
+
import os
from pathlib import Path
+from typing import Any, NoReturn
+
+from ._typing import TypeGuard
-def is_path(f):
+def is_path(f: Any) -> TypeGuard[bytes | str | Path]:
return isinstance(f, (bytes, str, Path))
-def is_directory(f):
+def is_directory(f: Any) -> TypeGuard[bytes | str | Path]:
"""Checks if an object is a string, and that it points to a directory."""
return is_path(f) and os.path.isdir(f)
class DeferredError:
- def __init__(self, ex):
+ def __init__(self, ex: BaseException):
self.ex = ex
- def __getattr__(self, elt):
+ def __getattr__(self, elt: str) -> NoReturn:
raise self.ex
+
+ @staticmethod
+ def new(ex: BaseException) -> Any:
+ """
+ Creates an object that raises the wrapped exception ``ex`` when used,
+ and casts it to :py:obj:`~typing.Any` type.
+ """
+ return DeferredError(ex)
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 279b6e228..0568943b5 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,2 +1,4 @@
# Master version for Pillow
-__version__ = "10.2.0.dev0"
+from __future__ import annotations
+
+__version__ = "10.3.0.dev0"
diff --git a/src/PIL/features.py b/src/PIL/features.py
index f14e60cf5..b14d6df13 100644
--- a/src/PIL/features.py
+++ b/src/PIL/features.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import collections
import os
import sys
diff --git a/src/_imaging.c b/src/_imaging.c
index 2270c77fe..59f80a354 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -2649,6 +2649,26 @@ _font_new(PyObject *self_, PyObject *args) {
self->glyphs[i].sy0 = S16(B16(glyphdata, 14));
self->glyphs[i].sx1 = S16(B16(glyphdata, 16));
self->glyphs[i].sy1 = S16(B16(glyphdata, 18));
+
+ // Do not allow glyphs to extend beyond bitmap image
+ // Helps prevent DOS by stopping cropped images being larger than the original
+ if (self->glyphs[i].sx0 < 0) {
+ self->glyphs[i].dx0 -= self->glyphs[i].sx0;
+ self->glyphs[i].sx0 = 0;
+ }
+ if (self->glyphs[i].sy0 < 0) {
+ self->glyphs[i].dy0 -= self->glyphs[i].sy0;
+ self->glyphs[i].sy0 = 0;
+ }
+ if (self->glyphs[i].sx1 > self->bitmap->xsize) {
+ self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize;
+ self->glyphs[i].sx1 = self->bitmap->xsize;
+ }
+ if (self->glyphs[i].sy1 > self->bitmap->ysize) {
+ self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize;
+ self->glyphs[i].sy1 = self->bitmap->ysize;
+ }
+
if (self->glyphs[i].dy0 < y0) {
y0 = self->glyphs[i].dy0;
}
@@ -2721,7 +2741,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) {
static PyObject *
_font_getmask(ImagingFontObject *self, PyObject *args) {
Imaging im;
- Imaging bitmap;
+ Imaging bitmap = NULL;
int x, b;
int i = 0;
int status;
@@ -2730,7 +2750,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
PyObject *encoded_string;
unsigned char *text;
- char *mode = "";
+ char *mode;
if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) {
return NULL;
@@ -2753,10 +2773,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
b = self->baseline;
for (x = 0; text[i]; i++) {
glyph = &self->glyphs[text[i]];
- bitmap =
- ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
- if (!bitmap) {
- goto failed;
+ if (i == 0 || text[i] != text[i - 1]) {
+ ImagingDelete(bitmap);
+ bitmap =
+ ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
+ if (!bitmap) {
+ goto failed;
+ }
}
status = ImagingPaste(
im,
@@ -2766,17 +2789,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
glyph->dy0 + b,
glyph->dx1 + x,
glyph->dy1 + b);
- ImagingDelete(bitmap);
if (status < 0) {
goto failed;
}
x = x + glyph->dx;
b = b + glyph->dy;
}
+ ImagingDelete(bitmap);
free(text);
return PyImagingNew(im);
failed:
+ ImagingDelete(bitmap);
free(text);
ImagingDelete(im);
Py_RETURN_NONE;
diff --git a/src/_imagingft.c b/src/_imagingft.c
index 64175de8b..6e24fcf95 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -877,22 +877,24 @@ font_render(FontObject *self, PyObject *args) {
width += stroke_width * 2 + ceil(x_start);
height += stroke_width * 2 + ceil(y_start);
- image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height);
+ image = PyObject_CallFunction(fill, "ii", width, height);
if (image == Py_None) {
PyMem_Del(glyph_info);
- return Py_BuildValue("ii", 0, 0);
+ return Py_BuildValue("N(ii)", image, 0, 0);
} else if (image == NULL) {
PyMem_Del(glyph_info);
return NULL;
}
- id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id"));
+ PyObject *imageId = PyObject_GetAttrString(image, "id");
+ id = PyLong_AsSsize_t(imageId);
+ Py_XDECREF(imageId);
im = (Imaging)id;
x_offset -= stroke_width;
y_offset -= stroke_width;
if (count == 0 || width == 0 || height == 0) {
PyMem_Del(glyph_info);
- return Py_BuildValue("ii", x_offset, y_offset);
+ return Py_BuildValue("N(ii)", image, x_offset, y_offset);
}
if (stroke_width) {
@@ -1047,8 +1049,8 @@ font_render(FontObject *self, PyObject *args) {
if (yy >= 0 && yy < im->ysize) {
/* blend this glyph into the buffer */
int k;
- unsigned char v;
unsigned char *target;
+ unsigned int tmp;
if (color) {
/* target[RGB] returns the color, target[A] returns the mask */
/* target bands get split again in ImageDraw.text */
@@ -1059,34 +1061,55 @@ font_render(FontObject *self, PyObject *args) {
if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
/* paste color glyph */
for (k = x0; k < x1; k++) {
- if (target[k * 4 + 3] < source[k * 4 + 3]) {
- /* unpremultiply BGRa to RGBA */
- target[k * 4 + 0] = CLIP8(
- (255 * (int)source[k * 4 + 2]) / source[k * 4 + 3]);
- target[k * 4 + 1] = CLIP8(
- (255 * (int)source[k * 4 + 1]) / source[k * 4 + 3]);
- target[k * 4 + 2] = CLIP8(
- (255 * (int)source[k * 4 + 0]) / source[k * 4 + 3]);
- target[k * 4 + 3] = source[k * 4 + 3];
+ unsigned int src_alpha = source[k * 4 + 3];
+
+ /* paste only if source has data */
+ if (src_alpha > 0) {
+ /* unpremultiply BGRa */
+ int src_red = CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha);
+ int src_green = CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha);
+ int src_blue = CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha);
+
+ /* blend required if target has data */
+ if (target[k * 4 + 3] > 0) {
+ /* blend RGBA colors */
+ target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], src_red, tmp);
+ target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], src_green, tmp);
+ target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp);
+ target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp));
+ } else {
+ /* paste unpremultiplied RGBA values */
+ target[k * 4 + 0] = src_red;
+ target[k * 4 + 1] = src_green;
+ target[k * 4 + 2] = src_blue;
+ target[k * 4 + 3] = src_alpha;
+ }
}
}
} else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
if (color) {
unsigned char *ink = (unsigned char *)&foreground_ink;
for (k = x0; k < x1; k++) {
- v = source[k] * convert_scale;
- if (target[k * 4 + 3] < v) {
- target[k * 4 + 0] = ink[0];
- target[k * 4 + 1] = ink[1];
- target[k * 4 + 2] = ink[2];
- target[k * 4 + 3] = v;
+ unsigned int src_alpha = source[k] * convert_scale;
+ if (src_alpha > 0) {
+ if (target[k * 4 + 3] > 0) {
+ target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], ink[0], tmp);
+ target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], ink[1], tmp);
+ target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], ink[2], tmp);
+ target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp));
+ } else {
+ target[k * 4 + 0] = ink[0];
+ target[k * 4 + 1] = ink[1];
+ target[k * 4 + 2] = ink[2];
+ target[k * 4 + 3] = src_alpha;
+ }
}
}
} else {
for (k = x0; k < x1; k++) {
- v = source[k] * convert_scale;
- if (target[k] < v) {
- target[k] = v;
+ unsigned int src_alpha = source[k] * convert_scale;
+ if (src_alpha > 0) {
+ target[k] = target[k] > 0 ? CLIP8(src_alpha + MULDIV255(target[k], (255 - src_alpha), tmp)) : src_alpha;
}
}
}
@@ -1107,18 +1130,12 @@ font_render(FontObject *self, PyObject *args) {
if (bitmap_converted_ready) {
FT_Bitmap_Done(library, &bitmap_converted);
}
- Py_DECREF(image);
FT_Stroker_Done(stroker);
PyMem_Del(glyph_info);
- return Py_BuildValue("ii", x_offset, y_offset);
+ return Py_BuildValue("N(ii)", image, x_offset, y_offset);
glyph_error:
- if (im->destroy) {
- im->destroy(im);
- }
- if (im->image) {
- free(im->image);
- }
+ Py_DECREF(image);
if (stroker != NULL) {
FT_Done_Glyph(glyph);
}
diff --git a/src/encode.c b/src/encode.c
index 08544aede..c7dd51015 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -1042,9 +1042,12 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
Py_ssize_t progressive = 0;
Py_ssize_t smooth = 0;
Py_ssize_t optimize = 0;
+ int keep_rgb = 0;
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
Py_ssize_t xdpi = 0, ydpi = 0;
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
+ Py_ssize_t restart_marker_blocks = 0;
+ Py_ssize_t restart_marker_rows = 0;
PyObject *qtables = NULL;
unsigned int *qarrays = NULL;
int qtablesLen = 0;
@@ -1057,17 +1060,20 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "ss|nnnnnnnnOz#y#y#",
+ "ss|nnnnpnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
&progressive,
&smooth,
&optimize,
+ &keep_rgb,
&streamtype,
&xdpi,
&ydpi,
&subsampling,
+ &restart_marker_blocks,
+ &restart_marker_rows,
&qtables,
&comment,
&comment_size,
@@ -1146,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8);
+ ((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb;
((JPEGENCODERSTATE *)encoder->state.context)->quality = quality;
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;
@@ -1156,6 +1163,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype;
((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi;
((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi;
+ ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_blocks = restart_marker_blocks;
+ ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = restart_marker_rows;
((JPEGENCODERSTATE *)encoder->state.context)->comment = comment;
((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size;
((JPEGENCODERSTATE *)encoder->state.context)->extra = extra;
diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c
index b08519d30..99d2a4ada 100644
--- a/src/libImaging/Convert.c
+++ b/src/libImaging/Convert.c
@@ -1013,7 +1013,7 @@ static struct {
static void
p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++) {
*out++ = (L(&palette->palette[in[x] * 4]) >= 128000) ? 255 : 0;
}
@@ -1022,7 +1022,7 @@ p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
pa2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++, in += 4) {
*out++ = (L(&palette->palette[in[0] * 4]) >= 128000) ? 255 : 0;
}
@@ -1031,7 +1031,7 @@ pa2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
p2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++) {
*out++ = L24(&palette->palette[in[x] * 4]) >> 16;
}
@@ -1040,7 +1040,7 @@ p2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++, in += 4) {
*out++ = L24(&palette->palette[in[0] * 4]) >> 16;
}
@@ -1070,7 +1070,7 @@ p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
p2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++, out += 4) {
const UINT8 *rgba = &palette->palette[*in++ * 4];
out[0] = out[1] = out[2] = L24(rgba) >> 16;
@@ -1081,7 +1081,7 @@ p2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
static void
pa2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
- /* FIXME: precalculate greyscale palette? */
+ /* FIXME: precalculate grayscale palette? */
for (x = 0; x < xsize; x++, in += 4, out += 4) {
out[0] = out[1] = out[2] = L24(&palette->palette[in[0] * 4]) >> 16;
out[3] = in[3];
@@ -1335,9 +1335,9 @@ topalette(
imOut->palette = ImagingPaletteDuplicate(palette);
if (imIn->bands == 1) {
- /* greyscale image */
+ /* grayscale image */
- /* Greyscale palette: copy data as is */
+ /* Grayscale palette: copy data as is */
ImagingSectionEnter(&cookie);
for (y = 0; y < imIn->ysize; y++) {
if (alpha) {
diff --git a/src/libImaging/Dib.c b/src/libImaging/Dib.c
index f8a2901b8..1b5bfe132 100644
--- a/src/libImaging/Dib.c
+++ b/src/libImaging/Dib.c
@@ -142,9 +142,9 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) {
GetSystemPaletteEntries(dib->dc, 0, 256, pal->palPalEntry);
if (strcmp(mode, "L") == 0) {
- /* Greyscale DIB. Fill all 236 slots with a greyscale ramp
+ /* Grayscale DIB. Fill all 236 slots with a grayscale ramp
* (this is usually overkill on Windows since VGA only offers
- * 6 bits greyscale resolution). Ignore the slots already
+ * 6 bits grayscale resolution). Ignore the slots already
* allocated by Windows */
i = 10;
@@ -160,7 +160,7 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) {
#ifdef CUBE216
/* Colour DIB. Create a 6x6x6 colour cube (216 entries) and
- * add 20 extra greylevels for best result with greyscale
+ * add 20 extra graylevels for best result with grayscale
* images. */
i = 10;
diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c
index f23245405..e37301df7 100644
--- a/src/libImaging/GifEncode.c
+++ b/src/libImaging/GifEncode.c
@@ -105,7 +105,7 @@ encode_loop:
st->head = st->codes[st->probe] >> 20;
goto encode_loop;
} else {
- /* Reprobe decrement must be nonzero and relatively prime to table
+ /* Reprobe decrement must be non-zero and relatively prime to table
* size. So, any odd positive number for power-of-2 size. */
if ((st->probe -= ((st->tail << 2) | 1)) < 0) {
st->probe += TABLE_SIZE;
diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h
index 1d7550818..7cdba9022 100644
--- a/src/libImaging/Jpeg.h
+++ b/src/libImaging/Jpeg.h
@@ -74,6 +74,9 @@ typedef struct {
/* Optimize Huffman tables (slow) */
int optimize;
+ /* Disable automatic conversion of RGB images to YCbCr if non-zero */
+ int keep_rgb;
+
/* Stream type (0=full, 1=tables only, 2=image only) */
int streamtype;
@@ -83,6 +86,10 @@ typedef struct {
/* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */
int subsampling;
+ /* Restart marker interval, in MCU blocks or MCU rows, or 0 for none */
+ unsigned int restart_marker_blocks;
+ unsigned int restart_marker_rows;
+
/* Converter input mode (input to the shuffler) */
char rawmode[8 + 1];
diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c
index 2a24eff39..00f3d5f74 100644
--- a/src/libImaging/JpegEncode.c
+++ b/src/libImaging/JpegEncode.c
@@ -137,6 +137,30 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
/* Compressor configuration */
jpeg_set_defaults(&context->cinfo);
+ /* Prevent RGB -> YCbCr conversion */
+ if (context->keep_rgb) {
+ switch (context->cinfo.in_color_space) {
+ case JCS_RGB:
+#ifdef JCS_EXTENSIONS
+ case JCS_EXT_RGBX:
+#endif
+ switch (context->subsampling) {
+ case -1: /* Default */
+ case 0: /* No subsampling */
+ break;
+ default:
+ /* Would subsample the green and blue
+ channels, which doesn't make sense */
+ state->errcode = IMAGING_CODEC_CONFIG;
+ return -1;
+ }
+ jpeg_set_colorspace(&context->cinfo, JCS_RGB);
+ break;
+ default:
+ break;
+ }
+ }
+
/* Use custom quantization tables */
if (context->qtables) {
int i;
@@ -210,6 +234,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
context->cinfo.smoothing_factor = context->smooth;
context->cinfo.optimize_coding = (boolean)context->optimize;
+ context->cinfo.restart_interval = context->restart_marker_blocks;
+ context->cinfo.restart_in_rows = context->restart_marker_rows;
if (context->xdpi > 0 && context->ydpi > 0) {
context->cinfo.write_JFIF_header = TRUE;
context->cinfo.density_unit = 1; /* dots per inch */
@@ -218,9 +244,9 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
switch (context->streamtype) {
case 1:
- /* tables only -- not yet implemented */
- state->errcode = IMAGING_CODEC_CONFIG;
- return -1;
+ /* tables only */
+ jpeg_write_tables(&context->cinfo);
+ goto cleanup;
case 2:
/* image only */
jpeg_suppress_tables(&context->cinfo, TRUE);
@@ -316,6 +342,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
jpeg_finish_compress(&context->cinfo);
+cleanup:
/* Clean up */
if (context->comment) {
free(context->comment);
diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c
index 14c8f1461..d47344245 100644
--- a/src/libImaging/Pack.c
+++ b/src/libImaging/Pack.c
@@ -549,16 +549,16 @@ static struct {
{"1", "1;IR", 1, pack1IR},
{"1", "L", 8, pack1L},
- /* greyscale */
+ /* grayscale */
{"L", "L", 8, copy1},
{"L", "L;16", 16, packL16},
{"L", "L;16B", 16, packL16B},
- /* greyscale w. alpha */
+ /* grayscale w. alpha */
{"LA", "LA", 16, packLA},
{"LA", "LA;L", 16, packLAL},
- /* greyscale w. alpha premultiplied */
+ /* grayscale w. alpha premultiplied */
{"La", "La", 16, packLA},
/* palette */
diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c
index 059d7b72a..78916bca5 100644
--- a/src/libImaging/Palette.c
+++ b/src/libImaging/Palette.c
@@ -75,7 +75,7 @@ ImagingPaletteNewBrowser(void) {
}
palette->size = i;
- /* FIXME: add 30-level greyscale wedge here? */
+ /* FIXME: add 30-level grayscale wedge here? */
return palette;
}
diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c
index c84acb998..398fbf6db 100644
--- a/src/libImaging/Quant.c
+++ b/src/libImaging/Quant.c
@@ -1697,7 +1697,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
image data? */
if (!strcmp(im->mode, "L")) {
- /* greyscale */
+ /* grayscale */
/* FIXME: converting a "L" image to "P" with 256 colors
should be done by a simple copy... */
diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c
index 128595f65..b1b03c515 100644
--- a/src/libImaging/Storage.c
+++ b/src/libImaging/Storage.c
@@ -80,18 +80,18 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->palette = ImagingPaletteNew("RGB");
} else if (strcmp(mode, "L") == 0) {
- /* 8-bit greyscale (luminance) images */
+ /* 8-bit grayscale (luminance) images */
im->bands = im->pixelsize = 1;
im->linesize = xsize;
} else if (strcmp(mode, "LA") == 0) {
- /* 8-bit greyscale (luminance) with alpha */
+ /* 8-bit grayscale (luminance) with alpha */
im->bands = 2;
im->pixelsize = 4; /* store in image32 memory */
im->linesize = xsize * 4;
} else if (strcmp(mode, "La") == 0) {
- /* 8-bit greyscale (luminance) with premultiplied alpha */
+ /* 8-bit grayscale (luminance) with premultiplied alpha */
im->bands = 2;
im->pixelsize = 4; /* store in image32 memory */
im->linesize = xsize * 4;
diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c
index 35122f182..e3b81590e 100644
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -14,6 +14,10 @@
#ifdef HAVE_LIBTIFF
+#ifdef HAVE_UNISTD_H
+#include /* lseek */
+#endif
+
#ifndef uint
#define uint uint32
#endif
diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h
index c7c7d48ed..02454ba03 100644
--- a/src/libImaging/TiffDecode.h
+++ b/src/libImaging/TiffDecode.h
@@ -13,12 +13,6 @@
#include
#endif
-/* UNDONE -- what are we using from this? */
-/*#ifndef _UNISTD_H
- # include
- # endif
-*/
-
#ifndef min
#define min(x, y) ((x > y) ? y : x)
#define max(x, y) ((x < y) ? y : x)
diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c
index 279bdcdc8..6c7d52f58 100644
--- a/src/libImaging/Unpack.c
+++ b/src/libImaging/Unpack.c
@@ -819,7 +819,7 @@ ImagingUnpackXBGR(UINT8 *_out, const UINT8 *in, int pixels) {
static void
unpackRGBALA(UINT8 *_out, const UINT8 *in, int pixels) {
int i;
- /* greyscale with alpha */
+ /* grayscale with alpha */
for (i = 0; i < pixels; i++) {
UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[1]);
memcpy(_out, &iv, sizeof(iv));
@@ -831,7 +831,7 @@ unpackRGBALA(UINT8 *_out, const UINT8 *in, int pixels) {
static void
unpackRGBALA16B(UINT8 *_out, const UINT8 *in, int pixels) {
int i;
- /* 16-bit greyscale with alpha, big-endian */
+ /* 16-bit grayscale with alpha, big-endian */
for (i = 0; i < pixels; i++) {
UINT32 iv = MAKE_UINT32(in[0], in[0], in[0], in[2]);
memcpy(_out, &iv, sizeof(iv));
@@ -1108,7 +1108,7 @@ unpackCMYKI(UINT8 *_out, const UINT8 *in, int pixels) {
/* There are two representations of LAB images for whatever precision:
L: Uint (in PS, it's 0-100)
A: Int (in ps, -128 .. 128, or elsewhere 0..255, with 128 as middle.
- Channels in PS display a 0 value as middle grey,
+ Channels in PS display a 0 value as middle gray,
LCMS appears to use 128 as the 0 value for these channels)
B: Int (as above)
@@ -1172,7 +1172,7 @@ unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) {
static void
unpackI12_I16(UINT8 *out, const UINT8 *in, int pixels) {
- /* Fillorder 1/MSB -> LittleEndian, for 12bit integer greyscale tiffs.
+ /* Fillorder 1/MSB -> LittleEndian, for 12bit integer grayscale tiffs.
According to the TIFF spec:
@@ -1527,7 +1527,7 @@ static struct {
{"1", "1;IR", 1, unpack1IR},
{"1", "1;8", 8, unpack18},
- /* greyscale */
+ /* grayscale */
{"L", "L;2", 2, unpackL2},
{"L", "L;2I", 2, unpackL2I},
{"L", "L;2R", 2, unpackL2R},
@@ -1544,11 +1544,11 @@ static struct {
{"L", "L;16", 16, unpackL16},
{"L", "L;16B", 16, unpackL16B},
- /* greyscale w. alpha */
+ /* grayscale w. alpha */
{"LA", "LA", 16, unpackLA},
{"LA", "LA;L", 16, unpackLAL},
- /* greyscale w. alpha premultiplied */
+ /* grayscale w. alpha premultiplied */
{"La", "La", 16, unpackLA},
/* palette */
diff --git a/tox.ini b/tox.ini
index 5388ed243..fb6746ce7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,3 +29,14 @@ pass_env =
commands =
pre-commit run --all-files --show-diff-on-failure
check-manifest
+
+[testenv:mypy]
+skip_install = true
+deps =
+ ipython
+ mypy==1.7.1
+ numpy
+extras =
+ typing
+commands =
+ mypy src {posargs}
diff --git a/wheels/README.md b/wheels/README.md
index c15c034b6..8b412b7fe 100644
--- a/wheels/README.md
+++ b/wheels/README.md
@@ -1,7 +1,11 @@
README
------
-This directory creates wheels for tagged versions of Pillow.
+[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux
+wheels for tagged versions of Pillow.
+
+This directory contains [multibuild](https://github.com/multi-build/multibuild) to
+build dependencies for the wheels, and dependency licenses to be included.
Archives
--------
@@ -16,8 +20,8 @@ But, the build will look in that repository before downloading from the
URL, so if there is a library that often fails to download, or you think might
fail to download, then download it and add it to the Git repository.
-See the `pre_build` in `config.sh` and the `fetch_unpack` routine in
-`multibuild/common_utils.sh` for the logic, and the build recipes in
+See `build` in `.github/workflows/wheels-dependencies.sh` and the `fetch_unpack`
+routine in `multibuild/common_utils.sh` for the logic, and the build recipes in
`multibuild/library_builders.sh` for the filename to give to the downloaded
archive.
@@ -27,5 +31,5 @@ Wheels
Wheels are
[GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml).
-Windows wheels are not created here. Instead, they are
+Windows wheels are created separately. They are
[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain).
diff --git a/wheels/config.sh b/wheels/config.sh
deleted file mode 100644
index 2943b0c4b..000000000
--- a/wheels/config.sh
+++ /dev/null
@@ -1,187 +0,0 @@
-# Define custom utilities
-# Test for macOS with [ -n "$IS_MACOS" ]
-
-ARCHIVE_SDIR=pillow-depends-main
-
-# Package versions for fresh source builds
-FREETYPE_VERSION=2.13.2
-HARFBUZZ_VERSION=8.2.1
-LIBPNG_VERSION=1.6.40
-JPEGTURBO_VERSION=3.0.0
-OPENJPEG_VERSION=2.5.0
-XZ_VERSION=5.4.4
-TIFF_VERSION=4.6.0
-LCMS2_VERSION=2.15
-if [[ -n "$IS_MACOS" ]]; then
- GIFLIB_VERSION=5.1.4
-else
- GIFLIB_VERSION=5.2.1
-fi
-if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
- ZLIB_VERSION=1.3
-else
- ZLIB_VERSION=1.2.8
-fi
-LIBWEBP_VERSION=1.3.2
-BZIP2_VERSION=1.0.8
-LIBXCB_VERSION=1.16
-BROTLI_VERSION=1.1.0
-
-if [[ -n "$IS_MACOS" ]] && [[ "$PLAT" == "x86_64" ]]; then
- function build_openjpeg {
- local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz)
- (cd $out_dir \
- && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
- && make install)
- touch openjpeg-stamp
- }
-fi
-
-function build_brotli {
- local cmake=$(get_modern_cmake)
- local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz)
- (cd $out_dir \
- && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
- && make install)
- if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
- cp /usr/local/lib64/libbrotli* /usr/local/lib
- cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig
- fi
-}
-
-function pre_build {
- # Any stuff that you need to do before you start building the wheels
- # Runs in the root directory of this repository.
- curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
- untar pillow-depends-main.zip
-
- build_xz
- if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
- yum remove -y zlib-devel
- fi
- build_new_zlib
-
- if [ -n "$IS_MACOS" ]; then
- ORIGINAL_BUILD_PREFIX=$BUILD_PREFIX
- ORIGINAL_PKG_CONFIG_PATH=$PKG_CONFIG_PATH
- BUILD_PREFIX=`dirname $(dirname $(which python))`
- PKG_CONFIG_PATH="$BUILD_PREFIX/lib/pkgconfig"
- fi
- build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
- if [ -n "$IS_MACOS" ]; then
- build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
- build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
- build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
- cp venv/share/pkgconfig/xcb-proto.pc venv/lib/pkgconfig/xcb-proto.pc
- else
- sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
- fi
- build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
- if [ -n "$IS_MACOS" ]; then
- BUILD_PREFIX=$ORIGINAL_BUILD_PREFIX
- PKG_CONFIG_PATH=$ORIGINAL_PKG_CONFIG_PATH
- fi
-
- build_libjpeg_turbo
- if [[ -n "$IS_MACOS" ]]; then
- rm /usr/local/lib/libjpeg.dylib
- fi
- build_tiff
- build_libpng
- build_lcms2
- build_openjpeg
-
- ORIGINAL_CFLAGS=$CFLAGS
- CFLAGS="$CFLAGS -O3 -DNDEBUG"
- if [[ -n "$IS_MACOS" ]]; then
- CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
- fi
- build_libwebp
- CFLAGS=$ORIGINAL_CFLAGS
-
- build_brotli
-
- if [ -n "$IS_MACOS" ]; then
- # Custom freetype build
- build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
- else
- build_freetype
- fi
-
- if [ -z "$IS_MACOS" ]; then
- export FREETYPE_LIBS=-lfreetype
- export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
- fi
- build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
- if [ -z "$IS_MACOS" ]; then
- export FREETYPE_LIBS=''
- export FREETYPE_CFLAGS=''
- fi
-
- # Append licenses
- for filename in wheels/dependency_licenses/*; do
- echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE
- cat $filename >> LICENSE
- done
-}
-
-function pip_wheel_cmd {
- local abs_wheelhouse=$1
- if [ -z "$IS_MACOS" ]; then
- CFLAGS="$CFLAGS --std=c99" # for Raqm
- fi
- python3 -m pip wheel $(pip_opts) \
- -C raqm=enable -C raqm=vendor -C fribidi=vendor \
- -w $abs_wheelhouse --no-deps .
-}
-
-function run_tests_in_repo {
- # Run Pillow tests from within source repo
- python3 selftest.py
- python3 -m pytest
-}
-
-EXP_CODECS="jpg jpg_2000 libtiff zlib"
-EXP_MODULES="freetype2 littlecms2 pil tkinter webp"
-EXP_FEATURES="fribidi harfbuzz libjpeg_turbo raqm transp_webp webp_anim webp_mux xcb"
-
-function run_tests {
- if [ -n "$IS_MACOS" ]; then
- brew install fribidi
- export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
- elif [ -n "$IS_ALPINE" ]; then
- apk add curl fribidi
- else
- apt-get update
- apt-get install -y curl libfribidi0 libopenblas-dev pkg-config unzip
- fi
- if [ -z "$IS_ALPINE" ]; then
- python3 -m pip install numpy
- fi
- python3 -m pip install defusedxml olefile pyroma
-
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- untar pillow-test-images.zip
- mv test-images-main/* ../Tests/images
-
- # Runs tests on installed distribution from an empty directory
- (cd .. && run_tests_in_repo)
- # Test against expected codecs, modules and features
- local ret=0
- local codecs=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_codecs())))')
- if [ "$codecs" != "$EXP_CODECS" ]; then
- echo "Codecs should be: '$EXP_CODECS'; but are '$codecs'"
- ret=1
- fi
- local modules=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_modules())))')
- if [ "$modules" != "$EXP_MODULES" ]; then
- echo "Modules should be: '$EXP_MODULES'; but are '$modules'"
- ret=1
- fi
- local features=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_features())))')
- if [ "$features" != "$EXP_FEATURES" ]; then
- echo "Features should be: '$EXP_FEATURES'; but are '$features'"
- ret=1
- fi
- return $ret
-}
diff --git a/wheels/dependency_licenses/FREETYPE2.txt b/wheels/dependency_licenses/FREETYPE2.txt
index cca8d8ce1..93efc6126 100644
--- a/wheels/dependency_licenses/FREETYPE2.txt
+++ b/wheels/dependency_licenses/FREETYPE2.txt
@@ -38,3 +38,615 @@ the 'Old MIT' license, compatible to the above two licenses.
The MD5 checksum support (only used for debugging in development
builds) is in the public domain.
+
+--------------------------------------------------------------------------
+
+ The FreeType Project LICENSE
+ ----------------------------
+
+ 2006-Jan-27
+
+ Copyright 1996-2002, 2006 by
+ David Turner, Robert Wilhelm, and Werner Lemberg
+
+
+
+Introduction
+============
+
+ The FreeType Project is distributed in several archive packages;
+ some of them may contain, in addition to the FreeType font engine,
+ various tools and contributions which rely on, or relate to, the
+ FreeType Project.
+
+ This license applies to all files found in such packages, and
+ which do not fall under their own explicit license. The license
+ affects thus the FreeType font engine, the test programs,
+ documentation and makefiles, at the very least.
+
+ This license was inspired by the BSD, Artistic, and IJG
+ (Independent JPEG Group) licenses, which all encourage inclusion
+ and use of free software in commercial and freeware products
+ alike. As a consequence, its main points are that:
+
+ o We don't promise that this software works. However, we will be
+ interested in any kind of bug reports. (`as is' distribution)
+
+ o You can use this software for whatever you want, in parts or
+ full form, without having to pay us. (`royalty-free' usage)
+
+ o You may not pretend that you wrote this software. If you use
+ it, or only parts of it, in a program, you must acknowledge
+ somewhere in your documentation that you have used the
+ FreeType code. (`credits')
+
+ We specifically permit and encourage the inclusion of this
+ software, with or without modifications, in commercial products.
+ We disclaim all warranties covering The FreeType Project and
+ assume no liability related to The FreeType Project.
+
+
+ Finally, many people asked us for a preferred form for a
+ credit/disclaimer to use in compliance with this license. We thus
+ encourage you to use the following text:
+
+ """
+ Portions of this software are copyright © The FreeType
+ Project (www.freetype.org). All rights reserved.
+ """
+
+ Please replace with the value from the FreeType version you
+ actually use.
+
+
+Legal Terms
+===========
+
+0. Definitions
+--------------
+
+ Throughout this license, the terms `package', `FreeType Project',
+ and `FreeType archive' refer to the set of files originally
+ distributed by the authors (David Turner, Robert Wilhelm, and
+ Werner Lemberg) as the `FreeType Project', be they named as alpha,
+ beta or final release.
+
+ `You' refers to the licensee, or person using the project, where
+ `using' is a generic term including compiling the project's source
+ code as well as linking it to form a `program' or `executable'.
+ This program is referred to as `a program using the FreeType
+ engine'.
+
+ This license applies to all files distributed in the original
+ FreeType Project, including all source code, binaries and
+ documentation, unless otherwise stated in the file in its
+ original, unmodified form as distributed in the original archive.
+ If you are unsure whether or not a particular file is covered by
+ this license, you must contact us to verify this.
+
+ The FreeType Project is copyright (C) 1996-2000 by David Turner,
+ Robert Wilhelm, and Werner Lemberg. All rights reserved except as
+ specified below.
+
+1. No Warranty
+--------------
+
+ THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY
+ KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO
+ USE, OF THE FREETYPE PROJECT.
+
+2. Redistribution
+-----------------
+
+ This license grants a worldwide, royalty-free, perpetual and
+ irrevocable right and license to use, execute, perform, compile,
+ display, copy, create derivative works of, distribute and
+ sublicense the FreeType Project (in both source and object code
+ forms) and derivative works thereof for any purpose; and to
+ authorize others to exercise some or all of the rights granted
+ herein, subject to the following conditions:
+
+ o Redistribution of source code must retain this license file
+ (`FTL.TXT') unaltered; any additions, deletions or changes to
+ the original files must be clearly indicated in accompanying
+ documentation. The copyright notices of the unaltered,
+ original files must be preserved in all copies of source
+ files.
+
+ o Redistribution in binary form must provide a disclaimer that
+ states that the software is based in part of the work of the
+ FreeType Team, in the distribution documentation. We also
+ encourage you to put an URL to the FreeType web page in your
+ documentation, though this isn't mandatory.
+
+ These conditions apply to any software derived from or based on
+ the FreeType Project, not just the unmodified files. If you use
+ our work, you must acknowledge us. However, no fee need be paid
+ to us.
+
+3. Advertising
+--------------
+
+ Neither the FreeType authors and contributors nor you shall use
+ the name of the other for commercial, advertising, or promotional
+ purposes without specific prior written permission.
+
+ We suggest, but do not require, that you use one or more of the
+ following phrases to refer to this software in your documentation
+ or advertising materials: `FreeType Project', `FreeType Engine',
+ `FreeType library', or `FreeType Distribution'.
+
+ As you have not signed this license, you are not required to
+ accept it. However, as the FreeType Project is copyrighted
+ material, only this license, or another one contracted with the
+ authors, grants you the right to use, distribute, and modify it.
+ Therefore, by using, distributing, or modifying the FreeType
+ Project, you indicate that you understand and accept all the terms
+ of this license.
+
+4. Contacts
+-----------
+
+ There are two mailing lists related to FreeType:
+
+ o freetype@nongnu.org
+
+ Discusses general use and applications of FreeType, as well as
+ future and wanted additions to the library and distribution.
+ If you are looking for support, start in this list if you
+ haven't found anything to help you in the documentation.
+
+ o freetype-devel@nongnu.org
+
+ Discusses bugs, as well as engine internals, design issues,
+ specific licenses, porting, etc.
+
+ Our home page can be found at
+
+ https://www.freetype.org
+
+
+--- 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`:
+
+```
+License
+*******
+
+Copyright (C) 2001-2002 by Francesco Zappa Nardelli
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*** Portions of the driver (that is, bdflib.c and bdf.h):
+
+Copyright 2000 Computing Research Labs, New Mexico State University
+Copyright 2001-2002, 2011 Francesco Zappa Nardelli
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE COMPUTING RESEARCH LAB OR NEW MEXICO STATE UNIVERSITY BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
+OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+Credits
+*******
+
+This driver is based on excellent Mark Leisher's bdf library. If you
+find something good in this driver you should probably thank him, not
+me.
+```
+
+The following license details are part of `src/pcf/README`:
+
+```
+License
+*******
+
+Copyright (C) 2000 by Francesco Zappa Nardelli
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+Credits
+*******
+
+Keith Packard wrote the pcf driver found in XFree86. His work is at
+the same time the specification and the sample implementation of the
+PCF format. Undoubtedly, this driver is inspired from his work.
+```
diff --git a/winbuild/build.rst b/winbuild/build.rst
index a8e4ebaa6..cd3b559e7 100644
--- a/winbuild/build.rst
+++ b/winbuild/build.rst
@@ -27,7 +27,7 @@ Download and install:
* `Ninja `_
(optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component)
-* x86/x64: `Netwide Assembler (NASM) `_
+* x86/AMD64: `Netwide Assembler (NASM) `_
Any version of Visual Studio 2017 or newer should be supported,
including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019.
@@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build::
usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD]
[--depends PILLOW_DEPS]
- [--architecture {x86,x64,ARM64}] [--nmake]
+ [--architecture {x86,AMD64,ARM64}] [--nmake]
[--no-imagequant] [--no-fribidi]
Download and generate build scripts for Pillow dependencies.
@@ -55,7 +55,7 @@ Run ``build_prepare.py`` to configure the build::
--depends PILLOW_DEPS
directory used to store cached dependencies (default:
'winbuild\depends')
- --architecture {x86,x64,ARM64}
+ --architecture {x86,AMD64,ARM64}
build architecture (default: same as host Python)
--nmake build dependencies using NMake instead of Ninja
--no-imagequant skip GPL-licensed optional dependency libimagequant
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 22b5a3056..df33ea493 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -56,7 +56,9 @@ def cmd_nmake(
)
-def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]:
+def cmds_cmake(
+ target: str | tuple[str, ...] | list[str], *params, build_dir: str = "."
+) -> list[str]:
if not isinstance(target, str):
target = " ".join(target)
@@ -73,10 +75,11 @@ def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]:
"-DCMAKE_CXX_FLAGS=-nologo",
*params,
'-G "{cmake_generator}"',
- ".",
+ f'-B "{build_dir}"',
+ "-S .",
]
),
- f"{{cmake}} --build . --clean-first --parallel --target {target}",
+ f'{{cmake}} --build "{build_dir}" --clean-first --parallel --target {target}',
]
@@ -102,7 +105,7 @@ SF_PROJECTS = "https://sourceforge.net/projects"
ARCHITECTURES = {
"x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"},
- "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"},
+ "AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"},
"ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"},
}
@@ -110,14 +113,20 @@ ARCHITECTURES = {
DEPS = {
"libjpeg": {
"url": SF_PROJECTS
- + "/libjpeg-turbo/files/3.0.0/libjpeg-turbo-3.0.0.tar.gz/download",
- "filename": "libjpeg-turbo-3.0.0.tar.gz",
- "dir": "libjpeg-turbo-3.0.0",
+ + "/libjpeg-turbo/files/3.0.1/libjpeg-turbo-3.0.1.tar.gz/download",
+ "filename": "libjpeg-turbo-3.0.1.tar.gz",
+ "dir": "libjpeg-turbo-3.0.1",
"license": ["README.ijg", "LICENSE.md"],
"license_pattern": (
"(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n=========="
".+(libjpeg-turbo Licenses\n======================\n\n.+)$"
),
+ "patch": {
+ r"CMakeLists.txt": {
+ # libjpeg-turbo does not detect MSVC x86_arm64 cross-compiler correctly
+ 'if(MSVC_IDE AND CMAKE_GENERATOR_PLATFORM MATCHES "arm64")': "if({architecture} STREQUAL ARM64)", # noqa: E501
+ },
+ },
"build": [
*cmds_cmake(
("jpeg-static", "cjpeg-static", "djpeg-static"),
@@ -148,9 +157,9 @@ DEPS = {
"libs": [r"*.lib"],
},
"xz": {
- "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.4.tar.gz/download",
- "filename": "xz-5.4.4.tar.gz",
- "dir": "xz-5.4.4",
+ "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.5.tar.gz/download",
+ "filename": "xz-5.4.5.tar.gz",
+ "dir": "xz-5.4.5",
"license": "COPYING",
"build": [
*cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),
@@ -165,23 +174,22 @@ DEPS = {
"filename": "libwebp-1.3.2.tar.gz",
"dir": "libwebp-1.3.2",
"license": "COPYING",
+ "patch": {
+ r"src\enc\picture_csp_enc.c": {
+ # link against libsharpyuv.lib
+ '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501
+ }
+ },
"build": [
- cmd_rmdir(r"output\release-static"), # clean
- cmd_nmake(
- "Makefile.vc",
- "all",
- [
- "CFG=release-static",
- "RTLIBCFG=dynamic",
- "OBJDIR=output",
- "ARCH={architecture}",
- "LIBWEBP_BASENAME=webp",
- ],
+ *cmds_cmake(
+ "webp webpdemux webpmux",
+ "-DBUILD_SHARED_LIBS:BOOL=OFF",
+ "-DWEBP_LINK_STATIC:BOOL=OFF",
),
cmd_mkdir(r"{inc_dir}\webp"),
cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"),
],
- "libs": [r"output\release-static\{architecture}\lib\*.lib"],
+ "libs": [r"libsharpyuv.lib", r"libwebp*.lib"],
},
"libtiff": {
"url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz",
@@ -194,8 +202,8 @@ DEPS = {
"#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501
},
r"libtiff\tif_webp.c": {
- # link against webp.lib
- "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501
+ # link against libwebp.lib
+ "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501
},
r"test\CMakeLists.txt": {
"add_executable(test_write_read_tags ../placeholder.h)": "",
@@ -208,12 +216,12 @@ DEPS = {
*cmds_cmake(
"tiff",
"-DBUILD_SHARED_LIBS:BOOL=OFF",
+ "-DWebP_LIBRARY=libwebp",
'-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"',
)
],
"headers": [r"libtiff\tiff*.h"],
"libs": [r"libtiff\*.lib"],
- # "bins": [r"libtiff\*.dll"],
},
"libpng": {
"url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download",
@@ -239,7 +247,7 @@ DEPS = {
"libs": ["*.lib"],
},
"freetype": {
- "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.gz", # noqa: E501
+ "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.gz",
"filename": "freetype-2.13.2.tar.gz",
"dir": "freetype-2.13.2",
"license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"],
@@ -272,13 +280,12 @@ DEPS = {
cmd_xcopy("include", "{inc_dir}"),
],
"libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"],
- # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"],
},
"lcms2": {
- "url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download",
- "filename": "lcms2-2.15.tar.gz",
- "dir": "lcms2-2.15",
- "license": "COPYING",
+ "url": SF_PROJECTS + "/lcms/files/lcms/2.16/lcms2-2.16.tar.gz/download",
+ "filename": "lcms2-2.16.tar.gz",
+ "dir": "lcms2-2.16",
+ "license": "LICENSE",
"patch": {
r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": {
# default is /MD for x86 and /MT for x64, we need /MD always
@@ -321,7 +328,7 @@ DEPS = {
},
"libimagequant": {
# commit: Merge branch 'master' into msvc (matches 2.17.0 tag)
- "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", # noqa: E501
+ "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
"filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
"dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab",
"license": "COPYRIGHT",
@@ -329,6 +336,8 @@ DEPS = {
"CMakeLists.txt": {
"if(OPENMP_FOUND)": "if(false)",
"install": "#install",
+ # libimagequant does not detect MSVC x86_arm64 cross-compiler correctly
+ "if(${{CMAKE_SYSTEM_PROCESSOR}} STREQUAL ARM64)": "if({architecture} STREQUAL ARM64)", # noqa: E501
}
},
"build": [
@@ -339,9 +348,9 @@ DEPS = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/8.2.1.zip",
- "filename": "harfbuzz-8.2.1.zip",
- "dir": "harfbuzz-8.2.1",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/8.3.0.zip",
+ "filename": "harfbuzz-8.3.0.zip",
+ "dir": "harfbuzz-8.3.0",
"license": "COPYING",
"build": [
*cmds_cmake(
@@ -361,7 +370,14 @@ DEPS = {
"build": [
cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"),
cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
- *cmds_cmake("fribidi"),
+ # generated tab.i files cannot be cross-compiled
+ " ^&^& ".join(
+ [
+ "if {architecture}==ARM64 cmd /c call {vcvarsall} x86",
+ *cmds_cmake("fribidi-gen", "-DARCH=x86", build_dir="build_x86"),
+ ]
+ ),
+ *cmds_cmake("fribidi", "-DARCH={architecture}"),
],
"bins": [r"*.dll"],
},
@@ -369,12 +385,16 @@ DEPS = {
# based on distutils._msvccompiler from CPython 3.7.4
-def find_msvs() -> dict[str, str] | None:
+def find_msvs(architecture: str) -> dict[str, str] | None:
root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
if not root:
print("Program Files not found")
return None
+ requires = ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"]
+ if architecture == "ARM64":
+ requires += ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.ARM64"]
+
try:
vspath = (
subprocess.check_output(
@@ -384,8 +404,7 @@ def find_msvs() -> dict[str, str] | None:
),
"-latest",
"-prerelease",
- "-requires",
- "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
+ *requires,
"-property",
"installationPath",
"-products",
@@ -471,7 +490,7 @@ def extract_dep(url: str, filename: str) -> None:
msg = "Attempted Path Traversal in Zip File"
raise RuntimeError(msg)
zf.extractall(sources_dir)
- elif filename.endswith(".tar.gz") or filename.endswith(".tgz"):
+ elif filename.endswith((".tar.gz", ".tgz")):
with tarfile.open(file, "r:gz") as tgz:
for member in tgz.getnames():
member_abspath = os.path.abspath(os.path.join(sources_dir, member))
@@ -575,14 +594,19 @@ def build_dep(name: str) -> str:
def build_dep_all() -> None:
lines = [r'call "{build_dir}\build_env.cmd"']
+ gha_groups = "GITHUB_ACTIONS" in os.environ
for dep_name in DEPS:
print()
if dep_name in disabled:
print(f"Skipping disabled dependency {dep_name}")
continue
script = build_dep(dep_name)
+ if gha_groups:
+ lines.append(f"@echo ::group::Running {script}")
lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"')
lines.append("if errorlevel 1 echo Build failed! && exit /B 1")
+ if gha_groups:
+ lines.append("@echo ::endgroup::")
print()
lines.append("@echo All Pillow dependencies built successfully!")
write_script("build_dep_all.cmd", lines)
@@ -627,7 +651,7 @@ if __name__ == "__main__":
(
"ARM64"
if platform.machine() == "ARM64"
- else ("x86" if struct.calcsize("P") == 4 else "x64")
+ else ("x86" if struct.calcsize("P") == 4 else "AMD64")
),
),
help="build architecture (default: same as host Python)",
@@ -656,7 +680,7 @@ if __name__ == "__main__":
arch_prefs = ARCHITECTURES[args.architecture]
print("Target architecture:", args.architecture)
- msvs = find_msvs()
+ msvs = find_msvs(args.architecture)
if msvs is None:
msg = "Visual Studio not found. Please install Visual Studio 2017 or newer."
raise RuntimeError(msg)
diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake
index 27b8d17a8..b16e0784c 100644
--- a/winbuild/fribidi.cmake
+++ b/winbuild/fribidi.cmake
@@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.12)
project(fribidi)
+
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(lib)
function(extract_regex_1 var text regex)
@@ -27,12 +27,20 @@ function(fribidi_conf)
set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new")
set(SIZEOF_INT 4)
set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC")
- message("detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}")
- configure_file(lib/fribidi-config.h.in lib/fribidi-config.h @ONLY)
+ message("Detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}")
+ configure_file(lib/fribidi-config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/lib/fribidi-config.h @ONLY)
endfunction()
fribidi_conf()
+option(ARCH "Target architecture")
+if(${ARCH} STREQUAL ARM64)
+ set(GEN FALSE)
+else()
+ set(GEN TRUE)
+endif()
+message("Generate tab.i files: " ${GEN})
+
function(prepend var prefix)
set(out "")
foreach(f ${ARGN})
@@ -56,18 +64,20 @@ macro(fribidi_definitions _TGT)
endmacro()
function(fribidi_gen _NAME _OUTNAME _PARAM)
- set(_OUT lib/${_OUTNAME})
- prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN})
- add_executable(gen-${_NAME}
- gen.tab/gen-${_NAME}.c
- gen.tab/packtab.c)
- fribidi_definitions(gen-${_NAME})
- target_compile_definitions(gen-${_NAME}
- PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H)
- add_custom_command(
- COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT}
- DEPENDS ${_DEP}
- OUTPUT ${_OUT})
+ set(_OUT ${CMAKE_CURRENT_SOURCE_DIR}/lib/${_OUTNAME})
+ if(GEN)
+ prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN})
+ add_executable(gen-${_NAME}
+ gen.tab/gen-${_NAME}.c
+ gen.tab/packtab.c)
+ fribidi_definitions(gen-${_NAME})
+ target_compile_definitions(gen-${_NAME}
+ PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H)
+ add_custom_command(
+ COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT}
+ DEPENDS ${_DEP}
+ OUTPUT ${_OUT})
+ endif(GEN)
list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}")
set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE)
endfunction()
@@ -78,8 +88,10 @@ fribidi_gen(unicode-version fribidi-unicode-version.h ""
macro(fribidi_tab _NAME)
fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN})
- target_sources(gen-${_NAME}-tab
- PRIVATE lib/fribidi-unicode-version.h)
+ if(GEN)
+ target_sources(gen-${_NAME}-tab
+ PRIVATE lib/fribidi-unicode-version.h)
+ endif(GEN)
endmacro()
fribidi_tab(bidi-type unidata/UnicodeData.txt)
@@ -89,14 +101,16 @@ fribidi_tab(mirroring unidata/BidiMirroring.txt)
fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt)
fribidi_tab(brackets-type unidata/BidiBrackets.txt)
+add_custom_target(fribidi-gen DEPENDS ${FRIBIDI_SOURCES_GENERATED})
+
file(GLOB FRIBIDI_SOURCES lib/*.c)
file(GLOB FRIBIDI_HEADERS lib/*.h)
add_library(fribidi SHARED
- ${FRIBIDI_SOURCES}
- ${FRIBIDI_HEADERS}
- ${FRIBIDI_SOURCES_GENERATED})
+ ${FRIBIDI_SOURCES}
+ ${FRIBIDI_HEADERS}
+ ${FRIBIDI_SOURCES_GENERATED})
fribidi_definitions(fribidi)
target_compile_definitions(fribidi
- PUBLIC "-DFRIBIDI_BUILD")
+ PUBLIC "-DFRIBIDI_BUILD")