diff --git a/.appveyor.yml b/.appveyor.yml
index 0f5dea9c5..b0740b1ac 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -6,6 +6,7 @@ init:
# Uncomment previous line to get RDP access during the build.
environment:
+ COVERAGE_CORE: sysmon
EXECUTABLE: python.exe
TEST_OPTIONS:
DEPLOY: YES
@@ -14,7 +15,7 @@ environment:
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64
- ARCHITECTURE: x64
+ ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt
index dd61634cd..ccd6d87ed 100644
--- a/.ci/requirements-cibw.txt
+++ b/.ci/requirements-cibw.txt
@@ -1 +1 @@
-cibuildwheel==2.16.2
+cibuildwheel==2.16.5
diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt
new file mode 100644
index 000000000..ed3269460
--- /dev/null
+++ b/.ci/requirements-mypy.txt
@@ -0,0 +1 @@
+mypy==1.7.1
diff --git a/.coveragerc b/.coveragerc
index f71b6b1a2..018cc1cbf 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,15 +2,19 @@
[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
+ if TYPE_CHECKING:
+ @abc.abstractmethod
+ # Empty bodies in protocols or abstract methods
+ ^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$
+ ^\s*\.\.\.(\s*#.*)?$
[run]
omit =
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/FUNDING.yml b/.github/FUNDING.yml
index e0e6804bf..8fc6bd0ad 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1 @@
-tidelift: "pypi/Pillow"
+tidelift: "pypi/pillow"
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/docs.yml b/.github/workflows/docs.yml
index 9fe345c8a..92e860cb5 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -7,10 +7,12 @@ on:
paths:
- ".github/workflows/docs.yml"
- "docs/**"
+ - "src/PIL/**"
pull_request:
paths:
- ".github/workflows/docs.yml"
- "docs/**"
+ - "src/PIL/**"
workflow_dispatch:
permissions:
@@ -37,16 +39,26 @@ jobs:
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 9069fc615..cc4760288 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -23,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') }}
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index f41324c4b..28124d7f7 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -2,7 +2,16 @@
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"
# TODO Update condition when cffi supports 3.13
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
index 8fc7bd379..a8ddef22c 100644
--- a/.github/workflows/release-drafter.yml
+++ b/.github/workflows/release-drafter.yml
@@ -23,6 +23,6 @@ jobs:
runs-on: ubuntu-latest
steps:
# Drafts your next release notes as pull requests are merged into "main"
- - uses: release-drafter/release-drafter@v5
+ - uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/system-info.py b/.github/workflows/system-info.py
index 57f28c620..9e97b8971 100644
--- a/.github/workflows/system-info.py
+++ b/.github/workflows/system-info.py
@@ -6,6 +6,7 @@ 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
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 32ac6f65e..4526b9454 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:
@@ -28,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ COVERAGE_CORE: sysmon
+
jobs:
build:
runs-on: windows-latest
@@ -49,9 +50,8 @@ 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: >
gcc-g++
ghostscript
@@ -71,6 +71,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
@@ -82,13 +83,13 @@ jobs:
zlib-devel
- name: Add Lapack to PATH
- uses: egor-tensin/cleanup-path@v3
+ uses: egor-tensin/cleanup-path@v4
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
- 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 +98,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') }}
@@ -143,7 +144,7 @@ jobs:
bash.exe .ci/after_success.sh
- name: Upload coverage
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v3.1.5
with:
file: ./coverage.xml
flags: GHA_Cygwin
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index eb27b4bf7..f40286fe4 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:
@@ -103,7 +101,7 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v3.1.5
with:
flags: GHA_Docker
name: ${{ matrix.docker }}
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index 115c2e9be..b4e479f12 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:
@@ -28,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ COVERAGE_CORE: sysmon
+
jobs:
build:
runs-on: windows-latest
@@ -84,7 +85,7 @@ jobs:
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v3.1.5
with:
file: ./coverage.xml
flags: GHA_Windows
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 86cd5b5fa..c936be559 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:
@@ -26,13 +26,16 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ COVERAGE_CORE: sysmon
+
jobs:
build:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
- python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
+ python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"]
timeout-minutes: 30
@@ -66,8 +69,16 @@ jobs:
- name: Print build system information
run: python3 .github/workflows/system-info.py
- - 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 Python dependencies
+ run: >
+ python3 -m pip install
+ coverage>=7.4.2
+ defusedxml
+ olefile
+ pyroma
+ pytest
+ pytest-cov
+ pytest-timeout
- name: Install dependencies
id: install
@@ -89,7 +100,7 @@ jobs:
- name: Cache build
id: build-cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: winbuild\build
key:
@@ -202,7 +213,7 @@ jobs:
shell: pwsh
- name: Upload coverage
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v3.1.5
with:
file: ./coverage.xml
flags: GHA_Windows
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index aa0e25138..643273e58 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,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ COVERAGE_CORE: sysmon
+ FORCE_COLOR: 1
+
jobs:
build:
@@ -35,7 +37,7 @@ jobs:
fail-fast: false
matrix:
os: [
- "macos-latest",
+ "macos-14",
"ubuntu-latest",
]
python-version: [
@@ -49,11 +51,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 }}
@@ -67,17 +79,28 @@ jobs:
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')
@@ -86,6 +109,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
@@ -123,9 +150,9 @@ jobs:
.ci/after_success.sh
- name: Upload coverage
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v3.1.5
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-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 3ec314873..26bf2f6d6 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -72,13 +72,11 @@ function build {
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
+ 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
else
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
@@ -131,13 +129,13 @@ 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
+ # libxau and libxdmcp cause 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 remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd
brew install pkg-config
fi
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 76d42b470..1140aaaad 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -30,7 +30,64 @@ env:
FORCE_COLOR: 1
jobs:
- build:
+ build-1-QEMU-emulated-wheels:
+ name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
+ runs-on: ubuntu-latest
+ 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:
+ - 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:
@@ -39,18 +96,18 @@ jobs:
include:
- name: "macOS x86_64"
os: macos-latest
- archs: x86_64
+ cibw_arch: x86_64
macosx_deployment_target: "10.10"
- name: "macOS arm64"
os: macos-latest
- archs: arm64
+ cibw_arch: arm64
macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
os: ubuntu-latest
- archs: x86_64
+ cibw_arch: x86_64
- name: "manylinux_2_28 x86_64"
os: ubuntu-latest
- archs: x86_64
+ cibw_arch: x86_64
build: "*manylinux*"
manylinux: "manylinux_2_28"
steps:
@@ -62,12 +119,15 @@ jobs:
with:
python-version: "3.x"
- - name: Build wheels
+ - 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.archs }}
+ CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
@@ -75,24 +135,21 @@ jobs:
CIBW_TEST_SKIP: "*-macosx_arm64"
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
- name: dist
+ name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
path: ./wheelhouse/*.whl
windows:
- name: Windows ${{ matrix.arch }}
+ name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- - arch: x86
- cibw_arch: x86
- - arch: x64
- cibw_arch: AMD64
- - arch: ARM64
- cibw_arch: ARM64
+ - cibw_arch: x86
+ - cibw_arch: AMD64
+ - cibw_arch: ARM64
steps:
- uses: actions/checkout@v4
@@ -106,6 +163,10 @@ jobs:
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
@@ -114,12 +175,7 @@ jobs:
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
- & python.exe -m pip install -r .ci/requirements-cibw.txt
-
- # Cannot cross-compile FriBiDi (only used for tests)
- $FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}")
- if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" }
- & python.exe winbuild\build_prepare.py -v @FLAGS
+ & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
shell: pwsh
- name: Build wheels
@@ -146,6 +202,7 @@ jobs:
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
@@ -157,24 +214,16 @@ jobs:
shell: cmd
- name: Upload wheels
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: dist
+ name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl
- - name: Prepare to upload FriBiDi
- if: "matrix.arch != 'ARM64'"
- run: |
- mkdir fribidi\${{ matrix.arch }}
- copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }}
- shell: cmd
-
- name: Upload fribidi.dll
- if: "matrix.arch != 'ARM64'"
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: fribidi
- path: fribidi\*
+ name: fribidi-windows-${{ matrix.cibw_arch }}
+ path: winbuild\build\bin\fribidi*
sdist:
runs-on: ubuntu-latest
@@ -190,17 +239,26 @@ jobs:
- run: make sdist
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
- name: dist
+ name: dist-sdist
path: dist/*.tar.gz
- success:
- permissions:
- contents: none
- needs: [build, windows, sdist]
+ 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: Wheels Successful
+ name: Upload release to PyPI
+ environment:
+ name: release-pypi
+ url: https://pypi.org/p/Pillow
+ permissions:
+ id-token: write
steps:
- - name: Success
- run: echo Wheels Successful
+ - 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 d1c4b8015..c52fdcb55 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,17 +1,17 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.7
+ rev: v0.2.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
- rev: 23.12.0
+ rev: 24.1.1
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
- rev: 1.7.6
+ rev: 1.7.7
hooks:
- id: bandit
args: [--severity-level=high]
@@ -32,6 +32,7 @@ repos:
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
@@ -47,12 +48,12 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: 1.5.3
+ rev: 1.7.0
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
- rev: v0.15
+ rev: v0.16
hooks:
- id: validate-pyproject
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 8f8250809..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-if: tag IS present OR type = api
-
-env:
- global:
- - CIBW_ARCHS=aarch64
- - CIBW_SKIP=pp38-*
-
-language: python
-# Default Python version is usually 3.6
-python: "3.12"
-dist: jammy
-services: docker
-
-jobs:
- include:
- - name: "manylinux2014 aarch64"
- os: linux
- arch: arm64
- env:
- - CIBW_BUILD="*manylinux*"
- - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014
- - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014
- - name: "manylinux_2_28 aarch64"
- os: linux
- arch: arm64
- env:
- - CIBW_BUILD="*manylinux*"
- - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28
- - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28
- - name: "musllinux aarch64"
- os: linux
- arch: arm64
- env:
- - CIBW_BUILD="*musllinux*"
-
-install:
- - python3 -m pip install -r .ci/requirements-cibw.txt
-
-script:
- - python3 -m cibuildwheel --output-dir wheelhouse
- - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/"
-
-# Upload wheels to GitHub Releases
-deploy:
- provider: releases
- api_key: $GITHUB_RELEASE_TOKEN
- file_glob: true
- file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl"
- on:
- repo: python-pillow/Pillow
- tags: true
- skip_cleanup: true
diff --git a/CHANGES.rst b/CHANGES.rst
index f69c3ffa7..205ffa294 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,9 +2,90 @@
Changelog (Pillow)
==================
-10.2.0 (unreleased)
+10.3.0 (unreleased)
-------------------
+- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782
+ [evanmiller, radarhere]
+
+- Fixed reading FLI/FLC images with a prefix chunk #7804
+ [twolife]
+
+- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
+ [nik012003, radarhere]
+
+- Remove execute bit from ``setup.py`` #7760
+ [hugovk]
+
+- 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]
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/README.md b/README.md
index e11bd2faa..9776c40e2 100644
--- a/README.md
+++ b/README.md
@@ -48,9 +48,6 @@ As of 2019, Pillow development is
-
@@ -67,11 +64,11 @@ As of 2019, Pillow development is
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg">
-
+
-
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):
@@ -37,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))
diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py
index ac46ff1eb..e0057a2c2 100644
--- a/Tests/check_fli_oob.py
+++ b/Tests/check_fli_oob.py
@@ -1,4 +1,3 @@
-#!/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 0fabcb5d3..5c89efc76 100644
--- a/Tests/check_fli_overflow.py
+++ b/Tests/check_fli_overflow.py
@@ -1,10 +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_imaging_leaks.py b/Tests/check_imaging_leaks.py
index 8c17c051d..890167039 100755
--- a/Tests/check_imaging_leaks.py
+++ b/Tests/check_imaging_leaks.py
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
from __future__ import annotations
+
+from typing import Any, Callable
+
import pytest
from PIL import Image
@@ -12,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_leaks.py b/Tests/check_j2k_leaks.py
index 83a12e2c2..bbe35b591 100644
--- a/Tests/check_j2k_leaks.py
+++ b/Tests/check_j2k_leaks.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -19,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))
@@ -29,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 982f6ea74..dbdd5a4f5 100644
--- a/Tests/check_j2k_overflow.py
+++ b/Tests/check_j2k_overflow.py
@@ -1,10 +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 9afbff112..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
@@ -14,7 +12,6 @@
# version.
from __future__ import annotations
-
from PIL import Image
repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2")
diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py
index 3cd37c7af..5f290c6cd 100644
--- a/Tests/check_jpeg_leaks.py
+++ b/Tests/check_jpeg_leaks.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -110,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:
@@ -180,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 9b83798d5..a9ce79e57 100644
--- a/Tests/check_large_memory.py
+++ b/Tests/check_large_memory.py
@@ -1,5 +1,8 @@
from __future__ import annotations
+
import sys
+from pathlib import Path
+from types import ModuleType
import pytest
@@ -15,6 +18,7 @@ from PIL import Image
# 2.7 and 3.2.
+numpy: ModuleType | None
try:
import numpy
except ImportError:
@@ -27,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 0ff3de8dc..f4ca8d0aa 100644
--- a/Tests/check_large_memory_numpy.py
+++ b/Tests/check_large_memory_numpy.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
import sys
+from pathlib import Path
import pytest
@@ -23,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")
@@ -31,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 ee1d7d11f..84bda53ed 100644
--- a/Tests/check_libtiff_segfault.py
+++ b/Tests/check_libtiff_segfault.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -6,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 292fe4b7f..d65ba6abc 100644
--- a/Tests/check_png_dos.py
+++ b/Tests/check_png_dos.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import zlib
from io import BytesIO
@@ -7,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:
@@ -23,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()
@@ -35,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)
@@ -52,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 ebfaffa47..cf414d7ff 100644
--- a/Tests/check_release_notes.py
+++ b/Tests/check_release_notes.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
from pathlib import Path
diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py
index afe4cc3ee..4b91984f5 100644
--- a/Tests/check_wheel.py
+++ b/Tests/check_wheel.py
@@ -1,10 +1,11 @@
from __future__ import annotations
+
import sys
from PIL import features
-def test_wheel_modules():
+def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
# tkinter is not available in cibuildwheel installed CPython on Windows
@@ -18,13 +19,13 @@ def test_wheel_modules():
assert set(features.get_supported_modules()) == expected_modules
-def test_wheel_codecs():
+def test_wheel_codecs() -> None:
expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
assert set(features.get_supported_codecs()) == expected_codecs
-def test_wheel_features():
+def test_wheel_features() -> None:
expected_features = {
"webp_anim",
"webp_mux",
diff --git a/Tests/conftest.py b/Tests/conftest.py
index cd64bd755..e00d1f019 100644
--- a/Tests/conftest.py
+++ b/Tests/conftest.py
@@ -1,8 +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
@@ -13,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 2e990b709..41c76f87e 100755
--- a/Tests/createfontdatachunk.py
+++ b/Tests/createfontdatachunk.py
@@ -1,5 +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 b333c2fd4..b98883946 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -1,6 +1,7 @@
"""
Helper functions.
"""
+
from __future__ import annotations
import logging
@@ -11,6 +12,7 @@ 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
@@ -19,42 +21,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)
@@ -67,14 +58,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)}"
@@ -86,28 +81,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
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)}"
@@ -125,55 +124,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:
@@ -194,7 +206,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
@@ -216,7 +228,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()
@@ -228,17 +240,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: 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
@@ -259,29 +271,31 @@ def hopper(mode=None, cache={}):
return im.copy()
-def djpeg_available():
+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():
+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:
@@ -298,47 +312,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/2422.flc b/Tests/images/2422.flc
new file mode 100644
index 000000000..eed5fb59e
Binary files /dev/null and b/Tests/images/2422.flc differ
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/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/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/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/unsupported_bitcount_luminance.dds b/Tests/images/unsupported_bitcount.dds
similarity index 100%
rename from Tests/images/unsupported_bitcount_luminance.dds
rename to Tests/images/unsupported_bitcount.dds
diff --git a/Tests/images/unsupported_bitcount_rgb.dds b/Tests/images/unsupported_bitcount_rgb.dds
deleted file mode 100644
index 77d527507..000000000
Binary files a/Tests/images/unsupported_bitcount_rgb.dds and /dev/null differ
diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py
index 4e7c7deec..8788d7021 100755
--- a/Tests/oss-fuzz/fuzz_font.py
+++ b/Tests/oss-fuzz/fuzz_font.py
@@ -13,7 +13,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-from __future__ import annotations
import atheris
@@ -24,7 +23,7 @@ with atheris.instrument_imports():
import fuzzers
-def TestOneInput(data):
+def TestOneInput(data: bytes) -> None:
try:
fuzzers.fuzz_font(data)
except Exception:
@@ -33,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 e7cd0474a..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");
@@ -13,7 +11,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-from __future__ import annotations
import atheris
@@ -24,7 +21,7 @@ with atheris.instrument_imports():
import fuzzers
-def TestOneInput(data):
+def TestOneInput(data: bytes) -> None:
try:
fuzzers.fuzz_image(data)
except Exception:
@@ -33,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 3f3c1e388..d6c1fab71 100644
--- a/Tests/oss-fuzz/fuzzers.py
+++ b/Tests/oss-fuzz/fuzzers.py
@@ -1,22 +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:
@@ -25,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 68834045a..459cc1a37 100644
--- a/Tests/oss-fuzz/test_fuzzers.py
+++ b/Tests/oss-fuzz/test_fuzzers.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import subprocess
import sys
@@ -23,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:
@@ -54,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 c582dfad3..c3926250f 100644
--- a/Tests/test_000_sanity.py
+++ b/Tests/test_000_sanity.py
@@ -1,8 +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 62da26636..d19799a09 100644
--- a/Tests/test_binary.py
+++ b/Tests/test_binary.py
@@ -1,13 +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
@@ -15,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 bed8dc3a8..0ad496135 100644
--- a/Tests/test_bmp_reference.py
+++ b/Tests/test_bmp_reference.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import warnings
@@ -9,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: str, ext: str = ".bmp") -> list[str]:
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"):
@@ -28,7 +29,7 @@ def test_bad():
pass
-def test_questionable():
+def test_questionable() -> None:
"""These shouldn't crash/dos, but it's not well defined that these
are in spec"""
supported = [
@@ -55,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."""
@@ -79,7 +80,7 @@ def test_good():
"rgb32bf.bmp": "rgb24.png",
}
- def get_compare(f):
+ def get_compare(f: str) -> str:
name = os.path.split(f)[1]
if name in file_map:
return os.path.join(base, "html", file_map[name])
diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py
index e798cba3d..1f6ed6127 100644
--- a/Tests/test_box_blur.py
+++ b/Tests/test_box_blur.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageFilter
@@ -15,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: Image.Image, radius: float = 1, n: int = 1) -> Image.Image:
return image._new(image.im.box_blur((radius, radius), n))
-def assert_image(im, data, delta=0):
+def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None:
it = iter(im.getdata())
for data_row in data:
im_row = [next(it) for _ in range(im.size[0])]
@@ -36,7 +37,13 @@ def assert_image(im, data, delta=0):
next(it)
-def assert_blur(im, radius, data, passes=1, delta=0):
+def assert_blur(
+ im: Image.Image,
+ radius: float,
+ data: list[list[int]],
+ 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))
@@ -44,7 +51,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):
@@ -64,7 +71,7 @@ def test_color_modes():
box_blur(sample.convert("YCbCr"))
-def test_radius_0():
+def test_radius_0() -> None:
assert_blur(
sample,
0,
@@ -80,7 +87,7 @@ def test_radius_0():
)
-def test_radius_0_02():
+def test_radius_0_02() -> None:
assert_blur(
sample,
0.02,
@@ -97,7 +104,7 @@ def test_radius_0_02():
)
-def test_radius_0_05():
+def test_radius_0_05() -> None:
assert_blur(
sample,
0.05,
@@ -114,7 +121,7 @@ def test_radius_0_05():
)
-def test_radius_0_1():
+def test_radius_0_1() -> None:
assert_blur(
sample,
0.1,
@@ -131,7 +138,7 @@ def test_radius_0_1():
)
-def test_radius_0_5():
+def test_radius_0_5() -> None:
assert_blur(
sample,
0.5,
@@ -148,7 +155,7 @@ def test_radius_0_5():
)
-def test_radius_1():
+def test_radius_1() -> None:
assert_blur(
sample,
1,
@@ -165,7 +172,7 @@ def test_radius_1():
)
-def test_radius_1_5():
+def test_radius_1_5() -> None:
assert_blur(
sample,
1.5,
@@ -182,7 +189,7 @@ def test_radius_1_5():
)
-def test_radius_bigger_then_half():
+def test_radius_bigger_then_half() -> None:
assert_blur(
sample,
3,
@@ -199,7 +206,7 @@ def test_radius_bigger_then_half():
)
-def test_radius_bigger_then_width():
+def test_radius_bigger_then_width() -> None:
assert_blur(
sample,
10,
@@ -214,7 +221,7 @@ def test_radius_bigger_then_width():
)
-def test_extreme_large_radius():
+def test_extreme_large_radius() -> None:
assert_blur(
sample,
600,
@@ -229,7 +236,7 @@ def test_extreme_large_radius():
)
-def test_two_passes():
+def test_two_passes() -> None:
assert_blur(
sample,
1,
@@ -247,7 +254,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 448ba2fac..c8886a779 100644
--- a/Tests/test_color_lut.py
+++ b/Tests/test_color_lut.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
from array import array
+from types import ModuleType
import pytest
@@ -7,6 +9,7 @@ from PIL import Image, ImageFilter
from .helper import assert_image_equal
+numpy: ModuleType | None
try:
import numpy
except ImportError:
@@ -14,7 +17,9 @@ except ImportError:
class TestColorLut3DCoreAPI:
- def generate_identity_table(self, channels, size):
+ def generate_identity_table(
+ self, channels: int, size: int | tuple[int, int, int]
+ ) -> tuple[int, int, int, int, list[float]]:
if isinstance(size, tuple):
size_1d, size_2d, size_3d = size
else:
@@ -40,7 +45,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"):
@@ -100,7 +105,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(
@@ -135,7 +140,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(
@@ -166,7 +171,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)
@@ -187,7 +192,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",
@@ -223,7 +228,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",
@@ -246,7 +251,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",
@@ -269,7 +274,7 @@ class TestColorLut3DCoreAPI:
),
)
- def test_channels_order(self):
+ def test_channels_order(self) -> None:
g = Image.linear_gradient("L")
im = Image.merge(
"RGB",
@@ -294,7 +299,7 @@ class TestColorLut3DCoreAPI:
])))
# fmt: on
- def test_overflow(self):
+ def test_overflow(self) -> None:
g = Image.linear_gradient("L")
im = Image.merge(
"RGB",
@@ -347,7 +352,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])
@@ -375,7 +380,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"
@@ -393,7 +398,8 @@ 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:
+ assert numpy is not 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)
@@ -426,7 +432,8 @@ 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:
+ assert numpy is not None
g = Image.linear_gradient("L")
im = Image.merge(
"RGB",
@@ -465,7 +472,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) == ""
@@ -483,7 +490,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)
@@ -497,7 +504,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"
@@ -507,7 +514,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)
)
@@ -520,7 +527,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")
@@ -536,7 +543,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"):
@@ -551,7 +558,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"
)
@@ -562,7 +569,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)
@@ -570,7 +577,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)
@@ -582,7 +589,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
)
@@ -598,7 +605,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
)
@@ -612,7 +619,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)
)
@@ -628,7 +635,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 5275652f6..2c1de8bc3 100644
--- a/Tests/test_core_resources.py
+++ b/Tests/test_core_resources.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
import pytest
@@ -8,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))
@@ -21,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()
@@ -34,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()
@@ -62,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()
@@ -83,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)
@@ -95,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()
@@ -116,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)
@@ -131,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)
@@ -148,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)
@@ -165,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"})
@@ -186,6 +187,6 @@ class TestEnvVars:
{"PILLOW_BLOCKS_MAX": "wat"},
),
)
- def test_warnings(self, var):
+ def test_warnings(self, var: dict[str, str]) -> 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 391948d40..9c21efa45 100644
--- a/Tests/test_decompression_bomb.py
+++ b/Tests/test_decompression_bomb.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -11,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
@@ -32,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
@@ -41,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
@@ -50,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
@@ -74,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
@@ -82,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:
@@ -98,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 d45a6603c..584d8f91d 100644
--- a/Tests/test_deprecate.py
+++ b/Tests/test_deprecate.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import _deprecate
@@ -19,12 +20,12 @@ from PIL import _deprecate
),
],
)
-def test_version(version, expected):
+def test_version(version: int | None, expected: str) -> 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")
@@ -45,13 +46,13 @@ def test_unknown_version():
),
],
)
-def test_old_version(deprecated, plural, expected):
+def test_old_version(deprecated: str, plural: bool, expected: str) -> 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\."
@@ -60,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(
@@ -75,7 +76,7 @@ def test_replacement_and_action():
"Upgrade to new thing.",
],
)
-def test_action(action):
+def test_action(action: str) -> None:
expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Upgrade to new thing\."
@@ -84,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 8f0e4b418..8d2d198ff 100644
--- a/Tests/test_features.py
+++ b/Tests/test_features.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+
import io
import re
+from typing import Callable
import pytest
@@ -14,7 +16,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)
@@ -24,11 +26,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: str, function: Callable[[str], bool]) -> None:
version = features.version(name)
if not features.check(name):
assert version is None
@@ -46,56 +48,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: str) -> 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: str) -> 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
@@ -105,7 +107,7 @@ def test_unsupported_codec():
features.version_codec(codec)
-def test_unsupported_module():
+def test_unsupported_module() -> None:
# Arrange
module = "unsupported_module"
# Act / Assert
@@ -115,7 +117,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 60d951636..395165b36 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImageSequence, PngImagePlugin
@@ -7,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
@@ -44,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: str) -> 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)
@@ -83,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)
@@ -105,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:
@@ -130,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)
@@ -164,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
@@ -217,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
@@ -225,7 +228,7 @@ 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)
@@ -266,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
@@ -291,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
@@ -335,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: str) -> 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)
@@ -373,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))
@@ -387,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
@@ -411,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 = []
@@ -474,7 +477,7 @@ def test_apng_save_duration_loop(tmp_path):
assert im.info["duration"] == 600
-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))
@@ -575,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))
@@ -597,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))
@@ -665,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()
@@ -677,7 +680,9 @@ def test_seek_after_close():
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
@pytest.mark.parametrize("default_image", (True, False))
@pytest.mark.parametrize("duplicate", (True, False))
-def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):
+def test_different_modes_in_later_frames(
+ mode: str, default_image: bool, duplicate: bool, tmp_path: Path
+) -> None:
test_file = str(tmp_path / "temp.png")
im = Image.new("L", (1, 1))
@@ -689,3 +694,12 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat
)
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 4c1e38d1d..1e2f20c40 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -11,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")
@@ -19,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"):
@@ -68,7 +71,7 @@ def test_save(tmp_path):
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
],
)
-def test_crashes(test_file):
+def test_crashes(test_file: str) -> 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 4cc92c5f6..1eaff0c7d 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
import io
+from pathlib import Path
import pytest
@@ -13,8 +15,8 @@ from .helper import (
)
-def test_sanity(tmp_path):
- def roundtrip(im):
+def test_sanity(tmp_path: Path) -> None:
+ def roundtrip(im: Image.Image) -> None:
outfile = str(tmp_path / "temp.bmp")
im.save(outfile, "BMP")
@@ -34,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")
@@ -59,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)
@@ -71,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)
@@ -79,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()
@@ -91,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")
@@ -109,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))
@@ -117,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"
@@ -126,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:
@@ -138,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:
@@ -156,7 +158,7 @@ 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)
@@ -176,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)
@@ -192,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: str, length: int) -> None:
with open(file_name, "rb") as fp:
data = fp.read(length)
with Image.open(io.BytesIO(data)) as im:
@@ -200,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 5780232a2..3dd24533a 100644
--- a/Tests/test_file_bufrstub.py
+++ b/Tests/test_file_bufrstub.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import BufrStubImagePlugin, Image
@@ -8,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
@@ -19,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"
@@ -28,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
@@ -36,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")
@@ -46,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):
@@ -60,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 0da5d3824..813b444db 100644
--- a/Tests/test_file_container.py
+++ b/Tests/test_file_container.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import ContainerIO, Image
@@ -8,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:
@@ -34,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:
@@ -48,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:
@@ -63,7 +64,7 @@ def test_seek_mode_2():
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_read_n0(bytesmode):
+def test_read_n0(bytesmode: bool) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@@ -79,7 +80,7 @@ def test_read_n0(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_read_n(bytesmode):
+def test_read_n(bytesmode: bool) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@@ -95,7 +96,7 @@ def test_read_n(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_read_eof(bytesmode):
+def test_read_eof(bytesmode: bool) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@@ -111,7 +112,7 @@ def test_read_eof(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_readline(bytesmode):
+def test_readline(bytesmode: bool) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
@@ -126,7 +127,7 @@ def test_readline(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False))
-def test_readlines(bytesmode):
+def test_readlines(bytesmode: bool) -> None:
# Arrange
expected = [
"This is line 1\n",
diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py
index 08c3257f9..dbf1b866d 100644
--- a/Tests/test_file_cur.py
+++ b/Tests/test_file_cur.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import CurImagePlugin, Image
@@ -6,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)
@@ -16,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 25e4badbc..65337cad9 100644
--- a/Tests/test_file_dcx.py
+++ b/Tests/test_file_dcx.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
@@ -11,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
@@ -24,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()
@@ -33,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
@@ -62,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
@@ -81,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 2d60fbb64..ebc0e89a1 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -1,6 +1,9 @@
"""Test DdsImagePlugin"""
+
from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
import pytest
@@ -32,6 +35,7 @@ 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"
@@ -44,7 +48,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
TEST_FILE_DX10_BC1_TYPELESS,
),
)
-def test_sanity_dxt1_bc1(image_path):
+def test_sanity_dxt1_bc1(image_path: str) -> None:
"""Check DXT1 and BC1 images can be opened"""
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
target = target.convert("RGBA")
@@ -58,7 +62,7 @@ def test_sanity_dxt1_bc1(image_path):
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:
@@ -71,7 +75,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:
@@ -92,7 +96,7 @@ def test_sanity_dxt5():
TEST_FILE_BC4U,
),
)
-def test_sanity_ati1_bc4u(image_path):
+def test_sanity_ati1_bc4u(image_path: str) -> None:
"""Check ATI1 and BC4U images can be opened"""
with Image.open(image_path) as im:
@@ -113,7 +117,7 @@ def test_sanity_ati1_bc4u(image_path):
TEST_FILE_DX10_BC4_TYPELESS,
),
)
-def test_dx10_bc4(image_path):
+def test_dx10_bc4(image_path: str) -> None:
"""Check DX10 BC4 images can be opened"""
with Image.open(image_path) as im:
@@ -134,7 +138,7 @@ def test_dx10_bc4(image_path):
TEST_FILE_BC5U,
),
)
-def test_sanity_ati2_bc5u(image_path):
+def test_sanity_ati2_bc5u(image_path: str) -> None:
"""Check ATI2 and BC5U images can be opened"""
with Image.open(image_path) as im:
@@ -158,7 +162,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: str, expected_path: str) -> None:
"""Check DX10 BC5 images can be opened"""
with Image.open(image_path) as im:
@@ -172,7 +176,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: str) -> None:
"""Check DX10 BC6H/BC6HS images can be opened"""
with Image.open(image_path) as im:
@@ -185,7 +189,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:
@@ -198,7 +202,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:
@@ -214,7 +218,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:
@@ -227,7 +231,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:
@@ -249,10 +253,11 @@ def test_dx10_r8g8b8a8_unorm_srgb():
("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: str, size: tuple[int, int], test_file: str) -> None:
"""Check uncompressed images can be opened"""
with Image.open(test_file) as im:
@@ -263,7 +268,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"
@@ -275,7 +280,7 @@ def test__accept_true():
assert output
-def test__accept_false():
+def test__accept_false() -> None:
"""Check invalid prefix"""
# Arrange
prefix = b"something invalid"
@@ -287,19 +292,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
@@ -307,13 +312,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()
@@ -321,7 +326,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:
@@ -336,21 +341,14 @@ 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")
-@pytest.mark.parametrize(
- "test_file",
- (
- "Tests/images/unsupported_bitcount_rgb.dds",
- "Tests/images/unsupported_bitcount_luminance.dds",
- ),
-)
-def test_unsupported_bitcount(test_file):
+def test_unsupported_bitcount() -> None:
with pytest.raises(OSError):
- with Image.open(test_file):
+ with Image.open("Tests/images/unsupported_bitcount.dds"):
pass
@@ -361,13 +359,13 @@ def test_unsupported_bitcount(test_file):
"Tests/images/unimplemented_pfflags.dds",
),
)
-def test_not_implemented(test_file):
+def test_not_implemented(test_file: str) -> None:
with pytest.raises(NotImplementedError):
with Image.open(test_file):
pass
-def test_save_unsupported_mode(tmp_path):
+def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds")
im = hopper("HSV")
with pytest.raises(OSError):
@@ -383,7 +381,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: str, test_file: str, 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 c479c384a..00f5f39e8 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
import io
+from pathlib import Path
import pytest
@@ -82,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: str, size: tuple[int, int], scale: int) -> None:
expected_size = tuple(s * scale for s in size)
with Image.open(filename) as image:
image.load(scale=scale)
@@ -92,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)
@@ -100,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:
@@ -114,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: bytes) -> 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: bytes) -> 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: bytes) -> 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: bytes) -> None:
data = io.BytesIO(
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
)
@@ -159,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: bytes) -> 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: bytes) -> 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: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
with Image.open(data) as img:
img.load()
@@ -186,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)
@@ -202,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:
@@ -213,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"
@@ -224,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:
@@ -232,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())
@@ -245,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):
@@ -259,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
@@ -270,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:
@@ -281,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
@@ -292,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:
@@ -303,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: str) -> None:
with Image.open(filename) as im:
new_size = (100, 100)
im = im.resize(new_size)
@@ -312,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: str) -> None:
# Issue #619
with Image.open(filename) as im:
new_size = (100, 100)
@@ -320,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: EpsImagePlugin.PSFile, ending: str) -> None:
ending = "Failure with line ending: %s" % (
"".join("%s" % ord(s) for s in ending)
)
@@ -342,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: str, ending: str) -> 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: str, ending: str) -> None:
f = str(tmp_path / "temp.txt")
with open(f, "wb") as w:
w.write(test_string.encode("latin-1"))
@@ -364,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)
@@ -374,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: bytes, line_ending: bytes) -> None:
simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
data = io.BytesIO(simple_file)
test_file = EpsImagePlugin.EpsImageFile(data)
@@ -392,14 +394,14 @@ def test_readline(prefix, line_ending):
"Tests/images/illuCS6_preview.eps",
),
)
-def test_open_eps(filename):
+def test_open_eps(filename: str) -> 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"
@@ -415,14 +417,14 @@ def test_emptyline():
"test_file",
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
)
-def test_timeout(test_file):
+def test_timeout(test_file: str) -> 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(
@@ -431,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 1383f9c5c..cce0b05cd 100644
--- a/Tests/test_file_fits.py
+++ b/Tests/test_file_fits.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -10,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
@@ -21,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"
@@ -30,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):
@@ -45,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 10bf36cc2..f86fb8d09 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -1,9 +1,10 @@
from __future__ import annotations
+
import warnings
import pytest
-from PIL import FliImagePlugin, Image
+from PIL import FliImagePlugin, Image, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
@@ -11,11 +12,14 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
# save as...-> hopper.fli, default options.
static_test_file = "Tests/images/hopper.fli"
-# From https://samples.libav.org/fli-flc/
+# From https://samples.ffmpeg.org/fli-flc/
animated_test_file = "Tests/images/a.fli"
+# From https://samples.ffmpeg.org/fli-flc/
+animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
-def test_sanity():
+
+def test_sanity() -> None:
with Image.open(static_test_file) as im:
im.load()
assert im.mode == "P"
@@ -31,9 +35,27 @@ def test_sanity():
assert im.is_animated
+def test_prefix_chunk() -> None:
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
+ try:
+ with Image.open(animated_test_file_with_prefix_chunk) as im:
+ assert im.mode == "P"
+ assert im.size == (320, 200)
+ assert im.format == "FLI"
+ assert im.info["duration"] == 171
+ assert im.is_animated
+
+ palette = im.getpalette()
+ assert palette[3:6] == [255, 255, 255]
+ assert palette[381:384] == [204, 204, 12]
+ assert palette[765:] == [252, 0, 0]
+ finally:
+ ImageFile.LOAD_TRUNCATED_IMAGES = False
+
+
@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()
@@ -41,14 +63,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()
@@ -57,13 +79,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
@@ -73,20 +95,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
@@ -96,7 +118,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
@@ -109,7 +131,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
@@ -131,7 +153,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)
@@ -146,7 +168,7 @@ def test_seek():
],
)
@pytest.mark.timeout(timeout=3)
-def test_timeouts(test_file):
+def test_timeouts(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(OSError):
@@ -159,7 +181,7 @@ def test_timeouts(test_file):
"Tests/images/crash-5762152299364352.fli",
],
)
-def test_crash(test_file):
+def test_crash(test_file: str) -> 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 af3b79815..e32f30a01 100644
--- a/Tests/test_file_fpx.py
+++ b/Tests/test_file_fpx.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -10,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)
@@ -19,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
@@ -29,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):
@@ -41,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 a494c8029..0c544245a 100644
--- a/Tests/test_file_ftex.py
+++ b/Tests/test_file_ftex.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import FtexImagePlugin, Image
@@ -6,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 7dfe05396..be98b08f2 100644
--- a/Tests/test_file_gbr.py
+++ b/Tests/test_file_gbr.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import GbrImagePlugin, Image
@@ -6,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)
@@ -19,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 ec80c54a1..d512df284 100644
--- a/Tests/test_file_gd.py
+++ b/Tests/test_file_gd.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import GdImageFile, UnidentifiedImageError
@@ -6,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 78b77e974..db9d3586c 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -1,6 +1,9 @@
from __future__ import annotations
+
import warnings
from io import BytesIO
+from pathlib import Path
+from typing import Generator
import pytest
@@ -22,7 +25,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"
@@ -32,8 +35,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()
@@ -41,14 +44,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()
@@ -61,20 +64,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
@@ -85,7 +88,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"
@@ -94,13 +97,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")
@@ -141,14 +144,14 @@ def test_strategy():
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
-def test_optimize():
- def test_grayscale(optimize):
+def test_optimize() -> None:
+ def test_grayscale(optimize: int) -> int:
im = Image.new("L", (1, 1), 0)
filename = BytesIO()
im.save(filename, "GIF", optimize=optimize)
return len(filename.getvalue())
- def test_bilevel(optimize):
+ def test_bilevel(optimize: int) -> int:
im = Image.new("1", (1, 1), 0)
test_file = BytesIO()
im.save(test_file, "GIF", optimize=optimize)
@@ -176,7 +179,9 @@ def test_optimize():
(4, 513, 256),
),
)
-def test_optimize_correctness(colors, size, expected_palette_length):
+def test_optimize_correctness(
+ colors: int, size: int, expected_palette_length: int
+) -> 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.
@@ -198,14 +203,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):
@@ -218,7 +223,7 @@ def test_optimize_if_palette_can_be_reduced_by_half():
assert len(reloaded.palette.palette) // 3 == colors
-def test_full_palette_second_frame(tmp_path):
+def test_full_palette_second_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("P", (1, 256))
@@ -239,7 +244,7 @@ def test_full_palette_second_frame(tmp_path):
reloaded.getpixel((0, i)) == i
-def test_roundtrip(tmp_path):
+def test_roundtrip(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = hopper()
im.save(out)
@@ -247,7 +252,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:
@@ -257,7 +262,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()
@@ -274,7 +279,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)
@@ -295,7 +300,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: str, mode: str) -> None:
with Image.open(path) as im:
assert im.mode == "P"
first_frame_colors = im.palette.colors.keys()
@@ -313,7 +318,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:
@@ -326,7 +331,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:
@@ -342,12 +347,12 @@ 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):
+ def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif")
- im.copy().save(out, *args, **kwargs)
+ im.copy().save(out, **kwargs)
reloaded = Image.open(out)
return reloaded
@@ -367,7 +372,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")
@@ -378,7 +383,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")
@@ -388,7 +393,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:
@@ -399,7 +404,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()
@@ -409,7 +414,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)
@@ -427,7 +432,7 @@ def test_seek_rewind():
("Tests/images/iss634.gif", 42),
),
)
-def test_n_frames(path, n_frames):
+def test_n_frames(path: str, n_frames: int) -> None:
# Test is_animated before n_frames
with Image.open(path) as im:
assert im.is_animated == (n_frames != 1)
@@ -438,7 +443,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)
@@ -459,7 +464,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
@@ -472,13 +477,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:
@@ -488,7 +493,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")
@@ -501,7 +506,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:
@@ -511,7 +516,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()
@@ -539,7 +544,10 @@ def test_dispose_background_transparency():
),
),
)
-def test_transparent_dispose(loading_strategy, expected_colors):
+def test_transparent_dispose(
+ loading_strategy: GifImagePlugin.LoadingStrategy,
+ expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],
+) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/transparent_dispose.gif") as img:
@@ -552,7 +560,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:
@@ -562,7 +570,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(
@@ -570,7 +578,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)
@@ -581,7 +589,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"),
@@ -609,7 +617,7 @@ 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, gray, black, red
@@ -640,7 +648,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
@@ -682,7 +690,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 = []
@@ -708,7 +716,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))]
@@ -726,7 +734,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
@@ -746,7 +754,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)
@@ -756,7 +764,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))
@@ -772,7 +780,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")
@@ -786,7 +794,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")
@@ -821,7 +829,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")
@@ -838,7 +846,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)] == [
@@ -854,7 +862,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")
@@ -887,7 +895,9 @@ def test_identical_frames(tmp_path):
1500,
),
)
-def test_identical_frames_to_single_frame(duration, tmp_path):
+def test_identical_frames_to_single_frame(
+ duration: int | list[int], tmp_path: Path
+) -> None:
out = str(tmp_path / "temp.gif")
im_list = [
Image.new("L", (100, 100), "#000"),
@@ -904,7 +914,7 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
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)
@@ -912,7 +922,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")
@@ -930,7 +940,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
@@ -939,7 +949,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
@@ -954,7 +964,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"
@@ -974,7 +984,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"
@@ -989,18 +999,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
@@ -1013,7 +1023,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):
@@ -1044,10 +1054,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: Image.Image, version: bytes) -> None:
im.save(out)
with Image.open(out) as reread:
assert reread.info["version"] == version
@@ -1074,7 +1084,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
@@ -1086,7 +1096,7 @@ def test_append_images(tmp_path):
assert reread.n_frames == 3
# Tests appending using a generator
- def im_generator(ims):
+ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
yield from ims
im.save(out, save_all=True, append_images=im_generator(ims))
@@ -1103,7 +1113,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,
@@ -1123,7 +1133,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))
@@ -1138,7 +1148,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
@@ -1160,7 +1170,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")
@@ -1171,13 +1181,13 @@ def test_rgba_transparency(tmp_path):
assert_image_equal(hopper("P").convert("RGB"), reloaded)
-def test_background_outside_palettte(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):
+def test_bbox(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("RGB", (100, 100), "#fff")
@@ -1188,7 +1198,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))
@@ -1200,7 +1210,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")
@@ -1214,7 +1224,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)
@@ -1228,7 +1238,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)
@@ -1241,7 +1251,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:
@@ -1264,7 +1274,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
@@ -1279,7 +1289,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")
@@ -1291,7 +1301,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)
@@ -1319,7 +1329,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
@@ -1327,7 +1337,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)
@@ -1339,7 +1349,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:
@@ -1347,7 +1357,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 d5be46dc3..006ee952d 100644
--- a/Tests/test_file_gimpgradient.py
+++ b/Tests/test_file_gimpgradient.py
@@ -1,8 +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
@@ -14,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
@@ -26,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
@@ -38,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
@@ -50,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
@@ -62,7 +63,7 @@ def test_curved():
assert ret == 0.75
-def test_sine():
+def test_sine() -> None:
# Arrange
middle = 0.5
pos = 0.75
@@ -74,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
@@ -86,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
@@ -98,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"
@@ -111,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 775d3b7cd..e8d5f1705 100644
--- a/Tests/test_file_gimppalette.py
+++ b/Tests/test_file_gimppalette.py
@@ -1,10 +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)
@@ -21,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 d962e85a4..096a5b88b 100644
--- a/Tests/test_file_gribstub.py
+++ b/Tests/test_file_gribstub.py
@@ -1,4 +1,8 @@
from __future__ import annotations
+
+from pathlib import Path
+from typing import IO
+
import pytest
from PIL import GribStubImagePlugin, Image
@@ -8,7 +12,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
@@ -19,7 +23,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"
@@ -28,7 +32,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
@@ -36,7 +40,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")
@@ -46,21 +50,21 @@ 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: Image.Image) -> None:
self.opened = True
- def load(self, im):
+ def load(self, im: Image.Image) -> Image.Image:
self.loaded = True
im.fp.close()
return Image.new("RGB", (1, 1))
- def save(self, im, fp, filename):
+ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
handler = TestHandler()
diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py
index 9c776b712..f871e2eff 100644
--- a/Tests/test_file_hdf5stub.py
+++ b/Tests/test_file_hdf5stub.py
@@ -1,4 +1,8 @@
from __future__ import annotations
+
+from pathlib import Path
+from typing import IO
+
import pytest
from PIL import Hdf5StubImagePlugin, Image
@@ -6,7 +10,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
@@ -17,7 +21,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"
@@ -26,7 +30,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
@@ -34,7 +38,7 @@ def test_load():
im.load()
-def test_save():
+def test_save() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
dummy_fp = None
@@ -47,21 +51,21 @@ 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: Image.Image) -> None:
self.opened = True
- def load(self, im):
+ def load(self, im: Image.Image) -> Image.Image:
self.loaded = True
im.fp.close()
return Image.new("RGB", (1, 1))
- def save(self, im, fp, filename):
+ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
handler = TestHandler()
diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py
index c62fffc5b..488984aef 100644
--- a/Tests/test_file_icns.py
+++ b/Tests/test_file_icns.py
@@ -1,7 +1,9 @@
from __future__ import annotations
+
import io
import os
import warnings
+from pathlib import Path
import pytest
@@ -13,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:
@@ -26,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)
@@ -34,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:
@@ -51,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))
@@ -66,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:
@@ -78,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:
@@ -95,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:
@@ -110,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.
@@ -126,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)
@@ -139,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 de9fa353a..f69a290fa 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+
import io
import os
+from pathlib import Path
import pytest
@@ -11,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"
@@ -20,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)])
@@ -72,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()
@@ -85,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")
@@ -99,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")
@@ -133,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: str) -> None:
output = io.BytesIO()
im = hopper(mode)
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
@@ -161,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:
@@ -180,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
@@ -196,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))
@@ -210,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):
@@ -218,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 0cb26d06a..036965bf5 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+
import filecmp
import warnings
+from pathlib import Path
import pytest
@@ -12,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"
@@ -20,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)
@@ -28,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()
@@ -37,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
@@ -60,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
@@ -80,14 +82,14 @@ def test_eoferror():
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
-def test_roundtrip(mode, tmp_path):
+def test_roundtrip(mode: str, 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)
@@ -99,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 3db488558..6957dfa0a 100644
--- a/Tests/test_file_imt.py
+++ b/Tests/test_file_imt.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import pytest
@@ -8,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 d0ecde393..88c30d468 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
from io import BytesIO, StringIO
@@ -6,12 +7,24 @@ 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
@@ -21,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
@@ -33,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())
@@ -50,7 +63,7 @@ def test_getiptcinfo_fotostation():
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"
@@ -63,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
@@ -73,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: pytest.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 ffaea6296..654242148 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -1,8 +1,12 @@
from __future__ import annotations
+
import os
import re
import warnings
from io import BytesIO
+from pathlib import Path
+from types import ModuleType
+from typing import Any
import pytest
@@ -30,6 +34,7 @@ from .helper import (
skip_unless_feature,
)
+ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
@@ -40,7 +45,7 @@ TEST_FILE = "Tests/images/hopper.jpg"
@skip_unless_feature("jpg")
class TestFileJpeg:
- def roundtrip(self, im, **options):
+ def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO()
im.save(out, "JPEG", **options)
test_bytes = out.tell()
@@ -49,7 +54,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: tuple[int, int], mode: str = "RGB") -> Image.Image:
"""Generates a very hard to compress file
:param size: tuple
:param mode: optional image mode
@@ -57,7 +62,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"))
@@ -69,13 +74,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: tuple[int, int], 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")
@@ -88,7 +93,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"
@@ -106,15 +111,13 @@ class TestFileJpeg:
assert "comment" not in reloaded.info
# Test that a comment argument overrides the default comment
- for comment in ("Test comment text", b"Text comment text"):
+ for comment in ("Test comment text", b"Test comment text"):
out = BytesIO()
im.save(out, format="JPEG", comment=comment)
with Image.open(out) as reloaded:
- if not isinstance(comment, bytes):
- comment = comment.encode()
- assert reloaded.info["comment"] == comment
+ assert reloaded.info["comment"] == b"Test comment text"
- 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"
@@ -142,12 +145,25 @@ class TestFileJpeg:
)
assert k > 0.9
+ def test_rgb(self) -> None:
+ def getchannels(im: Image.Image) -> tuple[int, int, int]:
+ 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(xdpi, ydpi=None):
+ def test_dpi(self, test_image_path: str) -> None:
+ def test(xdpi: int, ydpi: int | None = None):
with Image.open(test_image_path) as im:
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
return im.info.get("dpi")
@@ -160,7 +176,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"]
@@ -192,7 +208,7 @@ class TestFileJpeg:
ImageFile.MAXBLOCK * 4 + 3, # large block
),
)
- def test_icc_big(self, n):
+ def test_icc_big(self, n: int) -> 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
@@ -205,7 +221,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.
@@ -229,7 +245,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)
@@ -238,14 +254,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)
@@ -256,25 +272,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()
@@ -283,12 +299,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,
@@ -313,7 +329,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]
@@ -331,7 +347,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
@@ -341,7 +357,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
@@ -376,12 +392,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")
@@ -402,7 +418,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)
@@ -412,57 +428,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 getsampling(im):
+ def test_subsampling(self) -> None:
+ def getsampling(im: Image.Image):
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 subsampling1 in (0, "4:4:4"):
+ im = self.roundtrip(hopper(), subsampling=subsampling1)
+ assert getsampling(im) == (1, 1, 1, 1, 1, 1)
+ for subsampling1 in (1, "4:2:2"):
+ im = self.roundtrip(hopper(), subsampling=subsampling1)
+ assert getsampling(im) == (2, 1, 1, 1, 1, 1)
+ for subsampling1 in (2, "4:2:0", "4:1:1"):
+ im = self.roundtrip(hopper(), subsampling=subsampling1)
+ 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 subsampling1 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=subsampling1)
+ assert getsampling(im) == (1, 1, 1, 1, 1, 1)
+ for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3):
+ with pytest.raises(OSError):
+ self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1)
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")
@@ -476,13 +495,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
@@ -490,7 +509,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:
@@ -498,7 +517,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):
@@ -511,8 +530,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: int, test_file: str) -> None:
with Image.open(test_file) as im:
f = str(tmp_path / "temp.jpg")
im.save(f, qtables=[[n] * 64] * n)
@@ -620,24 +639,24 @@ 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
@@ -648,7 +667,7 @@ class TestFileJpeg:
"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):
+ def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None:
im = Image.new("RGB", (32, 32)) # 16 MCUs
out = BytesIO()
im.save(
@@ -662,20 +681,20 @@ class TestFileJpeg:
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()}
@@ -683,7 +702,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)
@@ -694,7 +713,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
@@ -706,20 +725,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: str) -> 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: str) -> 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:
@@ -731,7 +750,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))
@@ -744,7 +763,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)
@@ -752,7 +771,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
@@ -760,7 +779,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
@@ -768,7 +787,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
@@ -777,7 +796,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:
@@ -785,14 +804,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
@@ -802,7 +821,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:
@@ -813,7 +832,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
@@ -825,14 +844,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
@@ -840,10 +859,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,
@@ -860,14 +883,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
@@ -881,11 +904,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
@@ -904,7 +927,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(
@@ -933,7 +956,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(
@@ -944,7 +967,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(
@@ -956,16 +979,16 @@ 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):
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
return 0, 0
decoder = InfiniteMockPyDecoder(None)
- def closure(mode, *args):
+ def closure(mode: str, *args) -> InfiniteMockPyDecoder:
decoder.__init__(mode, *args)
return decoder
@@ -979,7 +1002,7 @@ class TestFileJpeg:
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
- def test_separate_tables(self):
+ def test_separate_tables(self) -> None:
im = hopper()
data = [] # [interchange, tables-only, image-only]
for streamtype in range(3):
@@ -1001,14 +1024,14 @@ class TestFileJpeg:
with Image.open(BytesIO(data[1] + data[2])) as combined_im:
assert_image_equal(interchange_im, combined_im)
- def test_repr_jpeg(self):
+ 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
@@ -1017,7 +1040,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 aaa4104e5..fab19e2ea 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -1,7 +1,10 @@
from __future__ import annotations
+
import os
import re
from io import BytesIO
+from pathlib import Path
+from typing import Any
import pytest
@@ -34,7 +37,7 @@ test_card.load()
# 'Not enough memory to handle tile data'
-def roundtrip(im, **options):
+def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO()
im.save(out, "JPEG2000", **options)
test_bytes = out.tell()
@@ -45,7 +48,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"))
@@ -58,20 +61,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)
@@ -81,7 +84,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")
@@ -89,54 +92,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: int) -> None:
d = 1 << (num_resolutions - 1)
im = test_card.resize((d - 1, d - 1))
with pytest.raises(OSError):
@@ -145,7 +148,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)
@@ -159,7 +162,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)
@@ -167,7 +170,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
@@ -177,7 +180,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",
@@ -191,17 +194,17 @@ 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)
- for quality_layers in ["quality_layers", ("100", "50", "10")]:
+ for quality_layers_str in ["quality_layers", ("100", "50", "10")]:
with pytest.raises(ValueError):
- test_card.save(outfile, quality_layers=quality_layers)
+ test_card.save(outfile, quality_layers=quality_layers_str)
-def test_layers():
+def test_layers() -> None:
out = BytesIO()
test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP")
out.seek(0)
@@ -231,7 +234,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: str, args: dict[str, bool], offset: int, data: bytes) -> None:
out = BytesIO()
if name:
out.name = name
@@ -240,7 +243,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()
@@ -261,7 +264,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))
@@ -276,7 +279,7 @@ def test_sgnd(tmp_path):
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
-def test_rgba(ext):
+def test_rgba(ext: str) -> None:
# Arrange
with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
# Act
@@ -287,47 +290,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: str) -> 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()
@@ -344,12 +347,12 @@ 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: str) -> None:
test = f"{EXTRA_DIR}/{name}.jp2"
reference = f"{EXTRA_DIR}/{name}.ppm"
with Image.open(test) as im:
- epsilon = 3 # for YCbCr images
+ epsilon = 3.0 # for YCbCr images
with Image.open(reference) as im2:
width, height = im2.size
if name[-1] == "2":
@@ -360,7 +363,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"
@@ -371,7 +374,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)
@@ -398,7 +401,7 @@ def test_save_comment():
"Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k",
],
)
-def test_crashes(test_file):
+def test_crashes(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
# Valgrind should not complain here
@@ -409,7 +412,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)
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 65adf449d..0994d9904 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import base64
import io
import itertools
@@ -6,6 +7,7 @@ import os
import re
import sys
from collections import namedtuple
+from pathlib import Path
import pytest
@@ -25,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: Image.Image) -> None:
"""Helper tests that assert basic sanity about the g4 tiff reading"""
# 1 bit
assert im.mode == "1"
@@ -49,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"
@@ -60,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"
@@ -74,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()
@@ -85,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()
@@ -97,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:
@@ -127,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"
@@ -138,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: bool, 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:
@@ -183,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.
@@ -240,8 +242,8 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.WRITE_LIBTIFF = False
- def test_custom_metadata(self, tmp_path):
- tc = namedtuple("test_case", "value,type,supported_by_default")
+ def test_custom_metadata(self, tmp_path: Path) -> None:
+ tc = namedtuple("tc", "value,type,supported_by_default")
custom = {
37000 + k: v
for k, v in enumerate(
@@ -282,7 +284,9 @@ class TestFileLibTiff(LibTiffTestCase):
for libtiff in libtiffs:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
- def check_tags(tiffinfo):
+ def check_tags(
+ tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
+ ) -> None:
im = hopper()
out = str(tmp_path / "temp.tif")
@@ -321,7 +325,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
@@ -329,7 +333,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")
@@ -340,7 +344,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")
@@ -350,7 +354,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")
@@ -359,7 +363,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"
@@ -378,7 +382,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"
@@ -395,7 +399,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:
@@ -408,7 +412,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
@@ -423,7 +427,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")
@@ -435,7 +439,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")
@@ -461,7 +465,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")
@@ -469,7 +473,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")
@@ -477,7 +481,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")
@@ -492,7 +496,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")
@@ -500,7 +504,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: Image.Image, tmp_path: Path) -> None:
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
@@ -512,14 +516,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: str, 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()
@@ -533,7 +537,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:
@@ -556,7 +560,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:
@@ -569,7 +573,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)
@@ -580,14 +584,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")
@@ -602,7 +606,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
@@ -635,7 +639,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.
@@ -645,7 +649,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Generate test image
pilim = hopper()
- def save_bytesio(compression=None):
+ def save_bytesio(compression: str | None = None) -> None:
buffer_io = io.BytesIO()
pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0)
@@ -660,7 +664,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")
@@ -669,7 +673,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
@@ -679,7 +683,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")
@@ -689,7 +693,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.
@@ -703,7 +707,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")
@@ -717,7 +721,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
@@ -728,8 +732,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: bool) -> None:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
@@ -748,7 +752,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"
@@ -764,7 +768,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")
@@ -776,7 +780,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)
@@ -792,7 +796,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)
@@ -804,7 +808,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:
@@ -817,14 +821,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
@@ -835,7 +839,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.mode == "F"
assert reloaded.getexif()[SAMPLEFORMAT] == 3
- def test_lzma(self, capfd):
+ def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None:
try:
with Image.open("Tests/images/hopper_lzma.tif") as im:
assert im.mode == "RGB"
@@ -851,7 +855,7 @@ class TestFileLibTiff(LibTiffTestCase):
sys.stderr.write(captured.err)
raise
- def test_webp(self, capfd):
+ def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None:
try:
with Image.open("Tests/images/hopper_webp.tif") as im:
assert im.mode == "RGB"
@@ -873,7 +877,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)
@@ -881,12 +885,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)
@@ -894,7 +898,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)
@@ -902,12 +906,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)
@@ -915,7 +919,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)
@@ -923,45 +927,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
@@ -969,7 +973,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: str | None, tmp_path: Path) -> None:
im = hopper()
out = str(tmp_path / "temp.tif")
@@ -985,11 +989,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:
@@ -1018,21 +1022,23 @@ class TestFileLibTiff(LibTiffTestCase):
),
],
)
- def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
+ def test_wrong_bits_per_sample(
+ self, file_name: str, mode: str, size: tuple[int, int], 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:
@@ -1043,7 +1049,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:
@@ -1052,7 +1058,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".
@@ -1073,7 +1079,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:
@@ -1084,7 +1090,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: str, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif")
im.save(out, compression=compression)
@@ -1094,14 +1100,14 @@ 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: bool, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif")
if not argument:
TiffImagePlugin.STRIP_SIZE = 2**18
try:
- arguments = {"compression": "tiff_adobe_deflate"}
+ arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, **arguments)
@@ -1112,13 +1118,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: str | None, 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):
@@ -1132,7 +1138,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: str, sizes: tuple[int, ...]) -> 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 9501c55a6..617e1e89c 100644
--- a/Tests/test_file_libtiff_small.py
+++ b/Tests/test_file_libtiff_small.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
from PIL import Image
@@ -7,7 +9,6 @@ from .test_file_libtiff import LibTiffTestCase
class TestFileLibTiffSmall(LibTiffTestCase):
-
"""The small lena image was failing on open in the libtiff
decoder because the file pointer was set to the wrong place
by a spurious seek. It wasn't failing with the byteio method.
@@ -16,7 +17,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"
@@ -25,7 +26,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()
@@ -36,7 +37,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 4b31aaa78..2c94fdc39 100644
--- a/Tests/test_file_mcidas.py
+++ b/Tests/test_file_mcidas.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, McIdasImagePlugin
@@ -6,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 e7ea39ea9..9a6f13ea3 100644
--- a/Tests/test_file_mic.py
+++ b/Tests/test_file_mic.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImagePalette
@@ -12,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"
@@ -27,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
@@ -52,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
@@ -62,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 da62bc6d4..4fb00d699 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+
import warnings
from io import BytesIO
+from typing import Any
import pytest
@@ -18,7 +20,7 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg")
-def roundtrip(im, **options):
+def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO()
im.save(out, "MPO", **options)
test_bytes = out.tell()
@@ -29,7 +31,7 @@ def roundtrip(im, **options):
@pytest.mark.parametrize("test_file", test_files)
-def test_sanity(test_file):
+def test_sanity(test_file: str) -> None:
with Image.open(test_file) as im:
im.load()
assert im.mode == "RGB"
@@ -38,8 +40,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()
@@ -47,14 +49,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()
@@ -62,14 +64,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: str) -> None:
# Test APP/COM reader (@PIL135)
with Image.open(test_file) as im:
assert im.applist[0][0] == "APP1"
@@ -81,7 +83,7 @@ def test_app(test_file):
@pytest.mark.parametrize("test_file", test_files)
-def test_exif(test_file):
+def test_exif(test_file: str) -> None:
with Image.open(test_file) as im_original:
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
@@ -92,7 +94,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:
@@ -105,7 +107,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:
@@ -119,7 +121,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()
@@ -132,7 +134,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]
@@ -142,14 +144,14 @@ def test_reload_exif_after_seek():
@pytest.mark.parametrize("test_file", test_files)
-def test_mp(test_file):
+def test_mp(test_file: str) -> 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:
@@ -158,7 +160,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:
@@ -167,7 +169,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: str) -> None:
with Image.open(test_file) as im:
mpinfo = im._getmp()
for frame_number, mpentry in enumerate(mpinfo[0xB002]):
@@ -184,7 +186,7 @@ def test_mp_attribute(test_file):
@pytest.mark.parametrize("test_file", test_files)
-def test_seek(test_file):
+def test_seek(test_file: str) -> None:
with Image.open(test_file) as im:
assert im.tell() == 0
# prior to first image raises an error, both blatant and borderline
@@ -208,13 +210,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
@@ -228,7 +230,7 @@ def test_eoferror():
@pytest.mark.parametrize("test_file", test_files)
-def test_image_grab(test_file):
+def test_image_grab(test_file: str) -> None:
with Image.open(test_file) as im:
assert im.tell() == 0
im0 = im.tobytes()
@@ -243,7 +245,7 @@ def test_image_grab(test_file):
@pytest.mark.parametrize("test_file", test_files)
-def test_save(test_file):
+def test_save(test_file: str) -> None:
with Image.open(test_file) as im:
assert im.tell() == 0
jpg0 = roundtrip(im)
@@ -254,7 +256,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 f4e357ae0..b0964aabe 100644
--- a/Tests/test_file_msp.py
+++ b/Tests/test_file_msp.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
import os
+from pathlib import Path
import pytest
@@ -12,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)
@@ -24,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"
@@ -41,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:
@@ -50,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: str, target_path: str) -> None:
with Image.open(source_path) as im:
assert_image_equal_tofile(im, target_path)
@@ -58,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)
@@ -71,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
@@ -79,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 735840de4..194f39b30 100644
--- a/Tests/test_file_palm.py
+++ b/Tests/test_file_palm.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+
import os.path
import subprocess
+from pathlib import Path
import pytest
@@ -9,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: str) -> None:
# Arrange
im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".palm"))
@@ -22,7 +24,7 @@ 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: list[str], tmp_path: Path, f: str) -> Image.Image:
outfile = str(tmp_path / "temp.png")
rc = subprocess.call(
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
@@ -31,7 +33,7 @@ def open_with_magick(magick, tmp_path, f):
return Image.open(outfile)
-def roundtrip(tmp_path, mode):
+def roundtrip(tmp_path: Path, mode: str) -> 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: str) -> 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 596a3414f..81a316fc1 100644
--- a/Tests/test_file_pcd.py
+++ b/Tests/test_file_pcd.py
@@ -1,8 +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 f42ec4a68..ab9f9663e 100644
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImageFile, PcxImagePlugin
@@ -6,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: Image.Image) -> None:
f = str(tmp_path / "temp.pcx")
im.save(f)
with Image.open(f) as im2:
@@ -17,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))
@@ -33,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):
@@ -41,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: str) -> 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.
@@ -50,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()
@@ -58,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"
@@ -70,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):
@@ -78,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):
@@ -86,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: Image.Image, size: int = 1024) -> None:
_last = ImageFile.MAXBLOCK
ImageFile.MAXBLOCK = size
try:
@@ -95,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):
@@ -104,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):
@@ -113,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):
@@ -124,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):
@@ -134,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):
@@ -146,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 9e07d9ed0..d39a86565 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -1,9 +1,12 @@
from __future__ import annotations
+
import io
import os
import os.path
import tempfile
import time
+from pathlib import Path
+from typing import Any, Generator
import pytest
@@ -12,7 +15,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: str, **kwargs: Any) -> str:
# Arrange
im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
@@ -39,17 +42,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: str) -> 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: str) -> 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:
@@ -65,7 +68,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"
@@ -74,7 +77,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")
@@ -82,7 +85,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")
@@ -110,7 +113,7 @@ def test_resolution(tmp_path):
{"dpi": (75, 150), "resolution": 200},
),
)
-def test_dpi(params, tmp_path):
+def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
@@ -134,7 +137,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)
@@ -154,7 +157,7 @@ def test_save_all(tmp_path):
assert os.path.getsize(outfile) > 0
# Test appending using a generator
- def im_generator(ims):
+ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
yield from ims
im.save(outfile, save_all=True, append_images=im_generator(ims))
@@ -170,7 +173,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")
@@ -180,7 +183,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))
@@ -217,14 +220,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: PdfParser.PdfParser) -> None:
pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info
assert b"Kids" in pages_info
@@ -242,7 +245,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")
@@ -293,7 +296,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,
@@ -322,7 +325,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")
@@ -337,7 +340,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: bytes) -> 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 63779f202..8f208cfbf 100644
--- a/Tests/test_file_pixar.py
+++ b/Tests/test_file_pixar.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, PixarImagePlugin
@@ -8,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"
@@ -20,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 ff3862110..c51f56ce7 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -1,9 +1,13 @@
from __future__ import annotations
+
import re
import sys
import warnings
import zlib
from io import BytesIO
+from pathlib import Path
+from types import ModuleType
+from typing import Any
import pytest
@@ -20,6 +24,7 @@ from .helper import (
skip_unless_feature,
)
+ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
@@ -34,7 +39,7 @@ TEST_PNG_FILE = "Tests/images/hopper.png"
MAGIC = PngImagePlugin._MAGIC
-def chunk(cid, *data):
+def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data)
return test_file.getvalue()
@@ -50,11 +55,11 @@ HEAD = MAGIC + IHDR
TAIL = IDAT + IEND
-def load(data):
+def load(data: bytes) -> Image.Image:
return Image.open(BytesIO(data))
-def roundtrip(im, **options):
+def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO()
im.save(out, "PNG", **options)
out.seek(0)
@@ -63,7 +68,7 @@ def roundtrip(im, **options):
@skip_unless_feature("zlib")
class TestFilePng:
- def get_chunks(self, filename):
+ def get_chunks(self, filename: str) -> list[bytes]:
chunks = []
with open(filename, "rb") as fp:
fp.read(8)
@@ -78,7 +83,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"))
@@ -101,13 +106,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.
@@ -116,7 +121,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)
@@ -134,7 +139,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)
@@ -155,7 +160,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 == {}
@@ -199,7 +204,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))
@@ -214,7 +219,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))
@@ -224,7 +229,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)
@@ -236,7 +241,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
@@ -257,7 +262,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
@@ -280,7 +285,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))
@@ -298,7 +303,7 @@ class TestFilePng:
assert_image(im, "RGBA", (10, 10))
assert im.getcolors() == [(100, (0, 0, 0, 0))]
- def test_save_grayscale_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:
@@ -319,13 +324,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:
@@ -338,7 +343,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)
@@ -354,7 +359,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")
@@ -371,7 +376,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
@@ -383,18 +388,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:
@@ -406,7 +411,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))
@@ -422,7 +427,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))
@@ -431,10 +436,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: str) -> None:
im = Image.new("RGB", (32, 32))
info = PngImagePlugin.PngInfo()
info.add_text("Text", value)
@@ -447,7 +452,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
@@ -461,7 +466,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.
@@ -476,7 +481,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
@@ -489,13 +494,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
@@ -505,40 +510,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))
@@ -559,17 +564,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:
@@ -587,7 +592,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 {
@@ -614,7 +619,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
@@ -634,7 +639,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: bytes) -> None:
fp = BytesIO()
with PngImagePlugin.PngStream(fp) as png:
with pytest.raises(ValueError):
@@ -644,7 +649,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")
@@ -653,7 +658,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))
@@ -663,7 +668,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(
@@ -678,7 +683,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()
@@ -704,7 +709,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:
@@ -724,7 +729,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())
@@ -733,7 +738,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")
@@ -741,11 +746,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)
@@ -753,7 +758,7 @@ class TestFilePng:
im.seek(1)
@pytest.mark.parametrize("buffer", (True, False))
- def test_save_stdout(self, buffer):
+ def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
if buffer:
@@ -785,7 +790,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))
@@ -793,7 +798,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 bb49a46d3..6e0fa32e4 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -1,18 +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)
@@ -63,7 +70,9 @@ def test_sanity():
),
),
)
-def test_arbitrary_maxval(data, mode, pixels):
+def test_arbitrary_maxval(
+ data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...]
+) -> None:
fp = BytesIO(data)
with Image.open(fp) as im:
assert im.size == (3, 1)
@@ -73,7 +82,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)
@@ -82,22 +91,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: bytes) -> None:
+ with pytest.raises(ValueError):
+ with Image.open(BytesIO(data)):
+ pass
@pytest.mark.parametrize(
@@ -117,12 +164,12 @@ def test_pnm(tmp_path):
),
),
)
-def test_plain(plain_path, raw_path):
+def test_plain(plain_path: str, raw_path: str) -> 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"
@@ -141,7 +188,9 @@ 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: bytes, data: bytes, comment_count: int
+) -> None:
path1 = str(tmp_path / "temp1.ppm")
path2 = str(tmp_path / "temp2.ppm")
comment = b"# comment" * comment_count
@@ -154,7 +203,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: bytes) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(data)
@@ -165,7 +214,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: bytes) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(data)
@@ -182,7 +231,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: bytes) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(data)
@@ -192,7 +241,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")
@@ -202,12 +251,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")
@@ -216,7 +265,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")
@@ -226,7 +275,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")
@@ -238,7 +287,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:
@@ -257,7 +306,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")
@@ -268,7 +317,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: bytes, tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n3 1 " + maxval)
@@ -280,7 +329,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
@@ -291,7 +340,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:
@@ -306,7 +355,7 @@ def test_mimetypes(tmp_path):
@pytest.mark.parametrize("buffer", (True, False))
-def test_save_stdout(buffer):
+def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout
if buffer:
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index 8b06ce2b1..e60638b22 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
@@ -10,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"
@@ -23,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()
@@ -32,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
@@ -63,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
@@ -77,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
@@ -94,30 +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_layer_skip():
+def test_layer_skip() -> None:
with Image.open("Tests/images/five_channels.psd") as im:
assert im.n_frames == 1
-def test_icc_profile():
+def test_icc_profile() -> None:
with Image.open(test_file) as im:
assert "icc_profile" in im.info
@@ -125,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.
@@ -156,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: str, 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 b7c945729..fd4b981ce 100644
--- a/Tests/test_file_qoi.py
+++ b/Tests/test_file_qoi.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, QoiImagePlugin
@@ -6,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)
@@ -22,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 13698276b..e13a8019e 100644
--- a/Tests/test_file_sgi.py
+++ b/Tests/test_file_sgi.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, SgiImagePlugin
@@ -11,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"
@@ -21,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"
@@ -35,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"
@@ -45,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"
@@ -54,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: Image.Image) -> None:
out = str(tmp_path / "temp.sgi")
img.save(out, format="sgi")
assert_image_equal_tofile(img, out)
@@ -88,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:
@@ -98,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 f21098754..75fef1dc6 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -1,7 +1,9 @@
from __future__ import annotations
+
import tempfile
import warnings
from io import BytesIO
+from pathlib import Path
import pytest
@@ -12,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"
@@ -21,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()
@@ -30,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()
@@ -58,7 +60,7 @@ def test_save(tmp_path):
assert im2.format == "SPIDER"
-def test_tempfile():
+def test_tempfile() -> None:
# Arrange
im = hopper()
@@ -74,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
@@ -88,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"]
@@ -108,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
@@ -119,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"
@@ -130,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):
@@ -138,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 874b37b52..6cfff8730 100644
--- a/Tests/test_file_sun.py
+++ b/Tests/test_file_sun.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import pytest
@@ -10,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"
@@ -27,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")
@@ -35,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 4470823cd..6217ebedd 100644
--- a/Tests/test_file_tar.py
+++ b/Tests/test_file_tar.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
@@ -18,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: str, test_path: str, format: str) -> None:
if features.check(codec):
with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar:
with Image.open(tar) as im:
@@ -29,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 d0f228573..3c6da50c5 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -1,7 +1,9 @@
from __future__ import annotations
+
import os
from glob import glob
from itertools import product
+from pathlib import Path
import pytest
@@ -20,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: str, tmp_path: Path) -> None:
+ def roundtrip(original_im: Image.Image) -> None:
out = str(tmp_path / "temp.tga")
original_im.save(out, rle=rle)
@@ -63,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")
@@ -73,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"
@@ -83,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"
@@ -93,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")
@@ -102,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")
@@ -119,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)
@@ -131,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")
@@ -139,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"
@@ -147,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")
@@ -178,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:
@@ -189,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)
@@ -198,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"
@@ -231,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 0851796d0..0110948ae 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -1,7 +1,11 @@
from __future__ import annotations
+
import os
import warnings
from io import BytesIO
+from pathlib import Path
+from types import ModuleType
+from typing import Generator
import pytest
@@ -18,6 +22,7 @@ from .helper import (
is_win32,
)
+ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
@@ -25,7 +30,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)
@@ -57,21 +62,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()
@@ -80,12 +85,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"
@@ -97,7 +102,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")
@@ -108,13 +113,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
@@ -127,7 +132,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
@@ -141,7 +146,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
@@ -154,14 +159,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: int | None, dpi: float) -> 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)
@@ -170,7 +175,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)
@@ -178,7 +183,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):
@@ -189,30 +194,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"
@@ -222,7 +227,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"
@@ -232,7 +237,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"
@@ -241,14 +246,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?"""
@@ -261,7 +266,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:
@@ -270,7 +275,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
@@ -282,12 +287,12 @@ class TestFileTiff:
("Tests/images/multipage.tiff", 3),
),
)
- def test_n_frames(self, path, n_frames):
+ def test_n_frames(self, path: str, n_frames: int) -> 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
@@ -299,7 +304,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
@@ -323,13 +328,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
@@ -342,7 +347,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
@@ -351,7 +356,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:
@@ -391,7 +396,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))
@@ -400,36 +405,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: bool) -> 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: Image.Exif) -> None:
assert sorted(exif.keys()) == [
256,
257,
@@ -480,19 +485,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]
@@ -500,7 +505,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)
@@ -509,7 +514,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: str, tmp_path: Path) -> None:
filename = str(tmp_path / "temp.tif")
im = hopper(mode)
im.save(filename, tiffinfo={262: 0})
@@ -517,13 +522,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
@@ -532,21 +537,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:
@@ -554,7 +559,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
@@ -587,7 +592,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)
@@ -600,7 +605,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"
@@ -612,25 +617,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
@@ -638,7 +651,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
@@ -650,7 +663,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: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
im = hopper(mode)
@@ -659,7 +672,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)
@@ -679,7 +692,7 @@ class TestFileTiff:
assert reread.n_frames == 3
# Test appending using a generator
- def im_generator(ims):
+ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
yield from ims
mp = BytesIO()
@@ -689,7 +702,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,
@@ -703,7 +716,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
@@ -714,14 +727,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:
@@ -732,7 +745,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(
@@ -747,7 +760,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,
@@ -773,7 +786,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")
@@ -786,7 +820,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:
@@ -808,7 +842,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):
@@ -816,7 +850,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()
@@ -829,7 +863,7 @@ class TestFileTiff:
],
)
@pytest.mark.timeout(2)
- def test_oom(self, test_file):
+ def test_oom(self, test_file: str) -> None:
with pytest.raises(UnidentifiedImageError):
with pytest.warns(UserWarning):
with Image.open(test_file):
@@ -838,7 +872,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 edd57e6b5..d7a18c725 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+
import io
import struct
+from pathlib import Path
import pytest
@@ -12,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
@@ -78,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),
@@ -119,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()
@@ -155,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
@@ -172,19 +177,21 @@ 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: bytes | int, expected: str, tmp_path: Path
+) -> None:
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271]
@@ -201,7 +208,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: int | IFDRational, tmp_path: Path) -> None:
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
@@ -217,7 +224,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()
@@ -233,7 +240,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
@@ -248,7 +255,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)
@@ -257,7 +264,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:
@@ -268,7 +275,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.
@@ -278,19 +285,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)
@@ -303,7 +310,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()
@@ -334,7 +341,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()
@@ -377,7 +384,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()
@@ -390,7 +397,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"
@@ -405,7 +412,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)
@@ -416,7 +423,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),
@@ -428,7 +435,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):
@@ -442,7 +449,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 0b84d0320..b34975e83 100644
--- a/Tests/test_file_wal.py
+++ b/Tests/test_file_wal.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import WalImageFile
from .helper import assert_image_equal_tofile
@@ -6,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"
@@ -18,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 c91818ef6..249846da4 100644
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -1,8 +1,10 @@
from __future__ import annotations
+
import io
import re
import sys
import warnings
+from pathlib import Path
import pytest
@@ -25,7 +27,7 @@ except ImportError:
class TestUnsupportedWebp:
- def test_unsupported(self):
+ def test_unsupported(self) -> None:
if HAVE_WEBP:
WebPImagePlugin.SUPPORTED = False
@@ -41,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?
@@ -66,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)
@@ -92,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?
@@ -100,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()
@@ -111,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")
@@ -123,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.
@@ -138,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.
@@ -147,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.
"""
@@ -165,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.
"""
@@ -176,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()
@@ -194,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")
@@ -226,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
@@ -237,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 79d01a444..a95434624 100644
--- a/Tests/test_file_webp_alpha.py
+++ b/Tests/test_file_webp_alpha.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -13,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?
@@ -38,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?
@@ -67,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?
@@ -98,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.
@@ -127,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 22acb4be6..9a730f1f9 100644
--- a/Tests/test_file_webp_animated.py
+++ b/Tests/test_file_webp_animated.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from packaging.version import parse as parse_version
@@ -17,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:
@@ -29,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.
@@ -59,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
@@ -104,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.
@@ -135,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
@@ -147,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.
@@ -178,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 6acf58ac3..32e29de56 100644
--- a/Tests/test_file_webp_lossless.py
+++ b/Tests/test_file_webp_lossless.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -9,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 a7b7bbcf6..875941240 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -1,5 +1,8 @@
from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
+from types import ModuleType
import pytest
@@ -12,13 +15,14 @@ pytestmark = [
skip_unless_feature("webp_mux"),
]
+ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
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"
@@ -36,7 +40,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"
@@ -48,7 +52,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:
@@ -62,7 +66,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"
@@ -79,7 +83,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:
@@ -99,7 +103,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:
@@ -112,7 +116,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() == {}
@@ -132,7 +136,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 596dc8ba1..b43e3f296 100644
--- a/Tests/test_file_wmf.py
+++ b/Tests/test_file_wmf.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, WmfImagePlugin
@@ -6,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"):
@@ -24,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()
@@ -50,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)
@@ -67,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 b086ffd68..44dd2541f 100644
--- a/Tests/test_file_xbm.py
+++ b/Tests/test_file_xbm.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
import pytest
@@ -31,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"
@@ -50,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"
@@ -62,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")
@@ -77,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 265feab42..26afe93f4 100644
--- a/Tests/test_file_xpm.py
+++ b/Tests/test_file_xpm.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, XpmImagePlugin
@@ -8,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"
@@ -19,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 5848995c1..6b8115930 100644
--- a/Tests/test_file_xvthumb.py
+++ b/Tests/test_file_xvthumb.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, XVThumbImagePlugin
@@ -8,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
@@ -19,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"
@@ -29,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 1e5eff2f1..136070f9e 100644
--- a/Tests/test_font_bdf.py
+++ b/Tests/test_font_bdf.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import BdfFontFile, FontFile
@@ -6,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)
@@ -14,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 388ee7118..b82340ef7 100644
--- a/Tests/test_font_crash.py
+++ b/Tests/test_font_crash.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageDraw, ImageFont
@@ -7,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")
@@ -17,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 6a038bb40..241f455b8 100644
--- a/Tests/test_font_leaks.py
+++ b/Tests/test_font_leaks.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image, ImageDraw, ImageFont
from .helper import PillowLeakTestCase, skip_unless_feature
@@ -9,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(
@@ -19,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)
@@ -29,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 4365b9310..997809e46 100644
--- a/Tests/test_font_pcf.py
+++ b/Tests/test_font_pcf.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
import os
+from pathlib import Path
import pytest
@@ -19,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)
@@ -28,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:
@@ -46,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)
@@ -58,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")
@@ -73,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):
@@ -89,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")
@@ -98,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 950e5029f..895458d9d 100644
--- a/Tests/test_font_pcf_charsets.py
+++ b/Tests/test_font_pcf_charsets.py
@@ -1,5 +1,8 @@
from __future__ import annotations
+
import os
+from pathlib import Path
+from typing import TypedDict
import pytest
@@ -13,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",
@@ -35,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)
@@ -44,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:
@@ -63,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")
@@ -79,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 fd47fae39..c07024a2c 100644
--- a/Tests/test_format_hsv.py
+++ b/Tests/test_format_hsv.py
@@ -1,30 +1,28 @@
from __future__ import annotations
+
import colorsys
import itertools
+from typing import Callable
from PIL import Image
from .helper import assert_image_similar, hopper
-def int_to_float(i):
+def int_to_float(i: int) -> float:
return i / 255
-def str_to_float(i):
- return ord(i) / 255
-
-
-def tuple_to_ints(tp):
+def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]:
x, y, z = 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))
-def wedge():
+def wedge() -> Image.Image:
w = Image._wedge()
w90 = w.rotate(90)
@@ -48,7 +46,11 @@ def wedge():
return img
-def to_xxx_colorsys(im, func, mode):
+def to_xxx_colorsys(
+ im: Image.Image,
+ func: Callable[[float, float, float], tuple[float, float, float]],
+ mode: str,
+) -> Image.Image:
# convert the hard way using the library colorsys routines.
(r, g, b) = im.split()
@@ -69,15 +71,15 @@ def to_xxx_colorsys(im, func, mode):
return hsv
-def to_hsv_colorsys(im):
+def to_hsv_colorsys(im: Image.Image) -> Image.Image:
return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV")
-def to_rgb_colorsys(im):
+def to_rgb_colorsys(im: Image.Image) -> Image.Image:
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)
@@ -109,7 +111,7 @@ def test_wedge():
)
-def test_convert():
+def test_convert() -> None:
im = hopper("RGB").convert("HSV")
comparable = to_hsv_colorsys(hopper("RGB"))
@@ -127,7 +129,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 c7610ce8a..4fcc37e88 100644
--- a/Tests/test_format_lab.py
+++ b/Tests/test_format_lab.py
@@ -1,8 +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()
@@ -23,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:
@@ -31,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 615e00e40..4c04e0da4 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import logging
import os
@@ -6,6 +7,8 @@ import shutil
import sys
import tempfile
import warnings
+from pathlib import Path
+from typing import IO
import pytest
@@ -59,19 +62,19 @@ class TestImage:
"HSV",
),
)
- def test_image_modes_success(self, mode):
+ def test_image_modes_success(self, mode: str) -> 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: str) -> 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:
class Pretty:
- def text(self, text):
+ def text(self, text: str) -> None:
self.pretty_output = text
im = Image.new("L", (100, 100))
@@ -107,7 +110,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"
@@ -129,7 +132,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
@@ -137,31 +140,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):
- from PIL.Image import Path
-
+ def test_pathlib(self, tmp_path: Path) -> None:
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
assert im.mode == "P"
assert im.size == (10, 10)
@@ -178,11 +179,13 @@ 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):
+ name: str
+
+ def write(self, b: bytes) -> None:
pass
fp = FP()
@@ -191,7 +194,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()
@@ -200,13 +203,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()
@@ -221,7 +224,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)
@@ -229,7 +232,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"))
@@ -240,7 +243,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
@@ -250,7 +253,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
@@ -263,7 +266,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
@@ -277,12 +280,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):
@@ -294,7 +297,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()
@@ -305,7 +308,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()
@@ -315,7 +318,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")
@@ -323,7 +326,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(
@@ -354,7 +357,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")
@@ -406,7 +409,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)
@@ -417,7 +420,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
@@ -427,7 +430,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"):
@@ -441,7 +444,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)
@@ -454,7 +457,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:
@@ -466,7 +469,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
@@ -484,7 +487,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
@@ -496,7 +499,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
@@ -507,7 +510,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
@@ -536,10 +539,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: tuple[int, int]) -> 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
@@ -548,13 +551,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"
@@ -563,7 +566,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: str) -> None:
# Arrange
target_file = "Tests/images/linear_gradient.png"
@@ -579,7 +582,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"
@@ -588,7 +591,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: str) -> None:
# Arrange
target_file = "Tests/images/radial_gradient.png"
@@ -604,7 +607,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:
@@ -620,7 +623,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))))
@@ -639,7 +642,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
@@ -654,7 +657,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")
@@ -663,7 +666,11 @@ 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.Image,
+ image: Image.Image,
+ palette_result: ImagePalette.ImagePalette | None = None,
+ ) -> None:
new_image = base_image._new(image.im)
assert new_image.mode == image.mode
assert new_image.size == image.size
@@ -678,7 +685,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)),
@@ -688,7 +695,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"
@@ -699,7 +706,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))
@@ -708,10 +715,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: IO[bytes]) -> None:
im = Image.open(fp)
im.load()
@@ -722,7 +729,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)
@@ -738,7 +745,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
@@ -784,7 +791,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 == {}
@@ -794,7 +801,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
@@ -806,7 +813,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}
@@ -822,7 +829,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) == {
@@ -836,7 +843,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) == {
@@ -848,7 +855,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]
@@ -857,7 +864,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"):
@@ -878,7 +885,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()
@@ -904,18 +911,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: tuple[int, int]) -> None:
im = Image.new("RGB", size)
assert im.tobytes() == b""
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
- def test_zero_frombytes(self, size):
+ def test_zero_frombytes(self, size: tuple[int, int]) -> None:
Image.frombytes("RGB", size, b"")
im = Image.new("RGB", size)
im.frombytes(b"")
- def test_has_transparency_data(self):
+ 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
@@ -940,7 +947,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}
@@ -969,7 +976,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,
@@ -994,7 +1001,7 @@ class TestImage:
"01r_00.pcx",
],
)
- def test_overrun(self, path):
+ def test_overrun(self, path: str) -> None:
"""For overrun completeness, test as:
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
"""
@@ -1008,7 +1015,7 @@ class TestImage:
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)
@@ -1016,7 +1023,12 @@ class TestImage:
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
- def test_close_graceful(self, caplog):
+ 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: pytest.LogCaptureFixture) -> None:
with Image.open("Tests/images/hopper.jpg") as im:
copy = im.copy()
with caplog.at_level(logging.DEBUG):
@@ -1027,17 +1039,17 @@ class TestImage:
class MockEncoder:
- pass
+ args: tuple[str, ...]
-def mock_encode(*args):
+def mock_encode(*args: str) -> MockEncoder:
encoder = MockEncoder()
encoder.args = args
return encoder
class TestRegistry:
- def test_encode_registry(self):
+ def test_encode_registry(self) -> None:
Image.register_encoder("MOCK", mock_encode)
assert "MOCK" in Image.ENCODERS
@@ -1046,6 +1058,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 4a794371d..380b89de8 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -1,8 +1,10 @@
from __future__ import annotations
+
import os
import subprocess
import sys
import sysconfig
+from types import ModuleType
import pytest
@@ -22,6 +24,7 @@ else:
except ImportError:
cffi = None
+numpy: ModuleType | None
try:
import numpy
except ImportError:
@@ -34,16 +37,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)
@@ -70,9 +73,10 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load()
pix2 = im2.load()
- for x, y in ((0, "0"), ("0", 0)):
- with pytest.raises(TypeError):
- pix1[x, y]
+ with pytest.raises(TypeError):
+ pix1[0, "0"]
+ with pytest.raises(TypeError):
+ pix1["0", 0]
for y in range(im1.size[1]):
for x in range(im1.size[0]):
@@ -80,7 +84,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)
@@ -118,16 +122,17 @@ 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()
+ assert numpy is not None
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
class TestImageGetPixel(AccessTest):
@staticmethod
- def color(mode):
+ def color(mode: str) -> int | tuple[int, ...]:
bands = Image.getmodebands(mode)
if bands == 1:
return 1
@@ -137,12 +142,13 @@ class TestImageGetPixel(AccessTest):
return (16, 32, 49)
return tuple(range(1, bands + 1))
- def check(self, mode, expected_color=None):
+ def check(self, mode: str, expected_color_int: int | None = None) -> None:
if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes")
- if not expected_color:
- expected_color = self.color(mode)
+ expected_color = (
+ self.color(mode) if expected_color_int is None else expected_color_int
+ )
# check putpixel
im = Image.new(mode, (1, 1), None)
@@ -221,25 +227,23 @@ class TestImageGetPixel(AccessTest):
"YCbCr",
),
)
- def test_basic(self, mode):
+ def test_basic(self, mode: str) -> None:
self.check(mode)
- def test_list(self):
+ def test_list(self) -> None:
im = hopper()
assert im.getpixel([0, 0]) == (20, 20, 70)
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
- @pytest.mark.parametrize(
- "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
- )
- def test_signedness(self, mode, expected_color):
+ @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
+ def test_signedness(self, mode: str, expected_color: int) -> 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: str, color: tuple[int, ...]) -> None:
im = Image.new(mode, (1, 1))
im.putpixel((0, 0), color)
@@ -263,7 +267,7 @@ class TestCffiGetPixel(TestImageGetPixel):
class TestCffi(AccessTest):
_need_cffi_access = True
- def _test_get_access(self, im):
+ def _test_get_access(self, im: Image.Image) -> None:
"""Do we get the same thing as the old pixel access
Using private interfaces, forcing a capi access and
@@ -281,7 +285,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()
@@ -300,7 +304,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: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image?
Using private interfaces, forcing a capi access and
@@ -321,7 +325,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()
@@ -344,11 +348,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):
@@ -360,7 +364,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: str) -> 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):
@@ -378,7 +382,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: str) -> None:
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"):
@@ -401,14 +405,16 @@ class TestImagePutPixelError(AccessTest):
),
),
)
- def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match):
+ def test_putpixel_invalid_number_of_bands(
+ self, mode: str, band_numbers: tuple[int, ...], match: str
+ ) -> 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: str) -> None:
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(
@@ -417,7 +423,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: str) -> None:
im = hopper(mode)
with pytest.raises(OverflowError):
im.putpixel((0, 0), 2**80)
@@ -426,10 +432,10 @@ 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
+ from setuptools.command import build_ext
with open("embed_pil.c", "w", encoding="utf-8") as fh:
fh.write(
@@ -458,7 +464,7 @@ int main(int argc, char* argv[])
% sys.prefix.replace("\\", "\\\\")
)
- compiler = new_compiler()
+ compiler = getattr(build_ext, "new_compiler")()
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
@@ -472,7 +478,7 @@ int main(int argc, char* argv[])
env["PATH"] = sys.prefix + ";" + env["PATH"]
# do not display the Windows Error Reporting dialog
- ctypes.windll.kernel32.SetErrorMode(0x0002)
+ getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002)
process = subprocess.Popen(["embed_pil.exe"], env=env)
process.communicate()
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
index b3e5d9e3e..cf85ee4fa 100644
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from typing import Any
+
import pytest
from packaging.version import parse as parse_version
@@ -11,12 +14,12 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
im = hopper().resize((128, 100))
-def test_toarray():
- def test(mode):
+def test_toarray() -> None:
+ def test(mode: str) -> tuple[tuple[int, ...], str, int]:
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
@@ -45,18 +48,18 @@ 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: Image.Image, arr_params: dict[str, Any]) -> None:
self.img = img
self.__array_interface__ = arr_params
- def tobytes(self):
+ def tobytes(self) -> bytes:
return self.img.tobytes()
- def test(mode):
+ def test(mode: str) -> tuple[str, tuple[int, int], bool]:
i = im.convert(mode)
a = numpy.array(i)
# Make wrapper instance for image, new array interface
@@ -88,7 +91,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 7c17040d3..f154de123 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -6,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
@@ -39,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()
@@ -61,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)
@@ -82,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"))
@@ -104,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
@@ -130,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
@@ -147,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
@@ -170,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))
@@ -200,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))
@@ -213,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
@@ -225,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)
@@ -235,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"
@@ -249,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
@@ -271,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
@@ -288,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)
@@ -316,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 abf5f846f..027e5338b 100644
--- a/Tests/test_image_copy.py
+++ b/Tests/test_image_copy.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import copy
import pytest
@@ -9,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)
@@ -38,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
@@ -46,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 0bb54e5d8..d095364ba 100644
--- a/Tests/test_image_crop.py
+++ b/Tests/test_image_crop.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -7,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)
@@ -16,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]:
@@ -46,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
@@ -68,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
@@ -86,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 774272dd1..1ce1a7cd8 100644
--- a/Tests/test_image_draft.py
+++ b/Tests/test_image_draft.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
from .helper import fromstring, skip_unless_feature, tostring
@@ -6,7 +7,12 @@ from .helper import fromstring, skip_unless_feature, tostring
pytestmark = skip_unless_feature("jpg")
-def draft_roundtrip(in_mode, in_size, req_mode, req_size):
+def draft_roundtrip(
+ in_mode: str,
+ in_size: tuple[int, int],
+ req_mode: str | None,
+ req_size: tuple[int, int] | None,
+) -> Image.Image:
im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG")
im = fromstring(data)
@@ -18,7 +24,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
@@ -47,7 +53,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"),
@@ -67,7 +73,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 031fceda3..c1dbb879b 100644
--- a/Tests/test_image_entropy.py
+++ b/Tests/test_image_entropy.py
@@ -1,9 +1,10 @@
from __future__ import annotations
+
from .helper import hopper
-def test_entropy():
- def entropy(mode):
+def test_entropy() -> None:
+ def entropy(mode: str) -> float:
return hopper(mode).entropy()
assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0
diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py
index 5bd7ee0d2..6a10ae453 100644
--- a/Tests/test_image_filter.py
+++ b/Tests/test_image_filter.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageFilter
@@ -35,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: ImageFilter.Filter, mode: str) -> None:
im = hopper(mode)
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
out = im.filter(filter_to_apply)
@@ -44,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: str) -> None:
with pytest.raises(TypeError):
im = hopper(mode)
im.filter("hello")
@@ -52,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: tuple[int, int]) -> None:
im = Image.new("RGB", size)
im.filter(ImageFilter.SMOOTH)
@@ -66,7 +67,10 @@ def test_crash(size):
("RGB", ((4, 0, 0), (0, 0, 0))),
),
)
-def test_modefilter(mode, expected):
+def test_modefilter(
+ mode: str,
+ expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]],
+) -> None:
im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9)))
# image is:
@@ -89,7 +93,13 @@ def test_modefilter(mode, expected):
("F", (0.0, 4.0, 8.0)),
),
)
-def test_rankfilter(mode, expected):
+def test_rankfilter(
+ mode: str,
+ expected: (
+ tuple[float, float, float]
+ | tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]
+ ),
+) -> None:
im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9)))
# image is:
@@ -105,7 +115,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: ImageFilter.RankFilter) -> None:
with pytest.raises(ValueError):
im = Image.new("P", (3, 3), None)
im.putdata(list(range(9)))
@@ -116,27 +126,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: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source:
reference_name = "hopper_emboss"
reference_name += "_I.png" if mode == "I" else ".bmp"
@@ -162,7 +172,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: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source:
reference_name = "hopper_emboss_more"
reference_name += "_I.png" if mode == "I" else ".bmp"
@@ -198,7 +208,7 @@ def test_consistency_5x5(mode):
(2, -2),
),
)
-def test_invalid_box_blur_filter(radius):
+def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None:
with pytest.raises(ValueError):
ImageFilter.BoxBlur(radius)
diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py
index 017da499d..98c0ea0b4 100644
--- a/Tests/test_image_frombytes.py
+++ b/Tests/test_image_frombytes.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -7,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: str) -> None:
im1 = hopper()
data = im1.tobytes()
diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py
index b3ca43bde..ea31a9de9 100644
--- a/Tests/test_image_fromqimage.py
+++ b/Tests/test_image_fromqimage.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
import warnings
+from typing import Generator
import pytest
@@ -17,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"),
@@ -30,7 +32,7 @@ def test_images():
im.close()
-def roundtrip(expected):
+def roundtrip(expected: Image.Image) -> None:
# PIL -> Qt
intermediate = expected.toqimage()
# Qt -> PIL
@@ -42,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 e7701dbc4..887553fc0 100644
--- a/Tests/test_image_getbands.py
+++ b/Tests/test_image_getbands.py
@@ -1,8 +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 9e792cfdf..18c6f6579 100644
--- a/Tests/test_image_getbbox.py
+++ b/Tests/test_image_getbbox.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -6,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))
@@ -33,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"):
@@ -44,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 dea3a60a1..8f8870f4f 100644
--- a/Tests/test_image_getcolors.py
+++ b/Tests/test_image_getcolors.py
@@ -1,9 +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)
@@ -38,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 873cc65bf..ac27400be 100644
--- a/Tests/test_image_getdata.py
+++ b/Tests/test_image_getdata.py
@@ -1,10 +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)
@@ -13,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 b17c8a786..a5b974459 100644
--- a/Tests/test_image_getextrema.py
+++ b/Tests/test_image_getextrema.py
@@ -1,11 +1,12 @@
from __future__ import annotations
+
from PIL import Image
from .helper import hopper
-def test_extrema():
- def extrema(mode):
+def test_extrema() -> None:
+ def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]:
return hopper(mode).getextrema()
assert extrema("1") == (0, 255)
@@ -19,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 e969c8164..9afa02b0a 100644
--- a/Tests/test_image_getim.py
+++ b/Tests/test_image_getim.py
@@ -1,8 +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 a5be972d3..6a8f157fc 100644
--- a/Tests/test_image_getpalette.py
+++ b/Tests/test_image_getpalette.py
@@ -1,11 +1,12 @@
from __future__ import annotations
+
from PIL import Image
from .helper import hopper
-def test_palette():
- def palette(mode):
+def test_palette() -> None:
+ def palette(mode: str) -> list[int] | None:
p = hopper(mode).getpalette()
if p:
return p[:10]
@@ -22,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 aa47be3b2..2b5a758ed 100644
--- a/Tests/test_image_getprojection.py
+++ b/Tests/test_image_getprojection.py
@@ -1,10 +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 7ba2f10b7..dbd55d4c2 100644
--- a/Tests/test_image_histogram.py
+++ b/Tests/test_image_histogram.py
@@ -1,9 +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 17847c4fd..0605821e0 100644
--- a/Tests/test_image_load.py
+++ b/Tests/test_image_load.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import logging
import os
@@ -9,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):
@@ -25,7 +26,7 @@ def test_close():
im.getpixel((0, 0))
-def test_close_after_load(caplog):
+def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None:
im = Image.open("Tests/images/hopper.gif")
im.load()
with caplog.at_level(logging.DEBUG):
@@ -33,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()
@@ -43,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 ad90d1250..8e94aafc5 100644
--- a/Tests/test_image_mode.py
+++ b/Tests/test_image_mode.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageMode
@@ -6,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
@@ -68,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 0b87f6072..2966f724f 100644
--- a/Tests/test_image_paste.py
+++ b/Tests/test_image_paste.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -7,13 +8,11 @@ from .helper import CachedProperty, assert_image_equal
class TestImagingPaste:
- masks = {}
size = 128
- def assert_9points_image(self, im, expected):
- expected = [
- point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected
- ]
+ def assert_9points_image(
+ self, im: Image.Image, expected: list[tuple[int, int, int, int]]
+ ) -> None:
px = im.load()
actual = [
px[0, 0],
@@ -26,9 +25,17 @@ class TestImagingPaste:
px[self.size // 2, self.size - 1],
px[self.size - 1, self.size - 1],
]
- assert actual == expected
+ assert actual == [
+ point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected
+ ]
- def assert_9points_paste(self, im, im2, mask, expected):
+ def assert_9points_paste(
+ self,
+ im: Image.Image,
+ im2: Image.Image,
+ mask: Image.Image,
+ expected: list[tuple[int, int, int, int]],
+ ) -> None:
im3 = im.copy()
im3.paste(im2, (0, 0), mask)
self.assert_9points_image(im3, expected)
@@ -38,7 +45,7 @@ class TestImagingPaste:
self.assert_9points_image(im, expected)
@CachedProperty
- def mask_1(self):
+ def mask_1(self) -> Image.Image:
mask = Image.new("1", (self.size, self.size))
px = mask.load()
for y in range(mask.height):
@@ -47,11 +54,11 @@ class TestImagingPaste:
return mask
@CachedProperty
- def mask_L(self):
+ def mask_L(self) -> Image.Image:
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
@CachedProperty
- def gradient_L(self):
+ def gradient_L(self) -> Image.Image:
gradient = Image.new("L", (self.size, self.size))
px = gradient.load()
for y in range(gradient.height):
@@ -60,7 +67,7 @@ class TestImagingPaste:
return gradient
@CachedProperty
- def gradient_RGB(self):
+ def gradient_RGB(self) -> Image.Image:
return Image.merge(
"RGB",
[
@@ -71,7 +78,7 @@ class TestImagingPaste:
)
@CachedProperty
- def gradient_LA(self):
+ def gradient_LA(self) -> Image.Image:
return Image.merge(
"LA",
[
@@ -81,7 +88,7 @@ class TestImagingPaste:
)
@CachedProperty
- def gradient_RGBA(self):
+ def gradient_RGBA(self) -> Image.Image:
return Image.merge(
"RGBA",
[
@@ -93,7 +100,7 @@ class TestImagingPaste:
)
@CachedProperty
- def gradient_RGBa(self):
+ def gradient_RGBa(self) -> Image.Image:
return Image.merge(
"RGBa",
[
@@ -105,7 +112,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_solid(self, mode):
+ def test_image_solid(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "red")
im2 = getattr(self, "gradient_" + mode)
@@ -115,7 +122,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: str) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -137,7 +144,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_mask_L(self, mode):
+ def test_image_mask_L(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -159,7 +166,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_mask_LA(self, mode):
+ def test_image_mask_LA(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -181,7 +188,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_mask_RGBA(self, mode):
+ def test_image_mask_RGBA(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -203,7 +210,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_image_mask_RGBa(self, mode):
+ def test_image_mask_RGBa(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
@@ -225,7 +232,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_color_solid(self, mode):
+ def test_color_solid(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "black")
rect = (12, 23, 128 + 12, 128 + 23)
@@ -238,7 +245,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: str) -> None:
im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)])
color = (10, 20, 30, 40)[: len(mode)]
@@ -260,7 +267,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_color_mask_L(self, mode):
+ def test_color_mask_L(self, mode: str) -> None:
im = getattr(self, "gradient_" + mode).copy()
color = "white"
@@ -282,7 +289,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_color_mask_RGBA(self, mode):
+ def test_color_mask_RGBA(self, mode: str) -> None:
im = getattr(self, "gradient_" + mode).copy()
color = "white"
@@ -304,7 +311,7 @@ class TestImagingPaste:
)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
- def test_color_mask_RGBa(self, mode):
+ def test_color_mask_RGBa(self, mode: str) -> None:
im = getattr(self, "gradient_" + mode).copy()
color = "white"
@@ -325,7 +332,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 fce45ec4f..05f209351 100644
--- a/Tests/test_image_point.py
+++ b/Tests/test_image_point.py
@@ -1,10 +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):
@@ -38,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
"""
@@ -46,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)]
@@ -57,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 0ba7e5919..2c92911d1 100644
--- a/Tests/test_image_putalpha.py
+++ b/Tests/test_image_putalpha.py
@@ -1,8 +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)
@@ -16,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
@@ -39,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 d3cb13e2e..73145faac 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
from array import array
@@ -9,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())
@@ -28,9 +29,9 @@ def test_sanity():
assert_image_equal(im1, im2)
-def test_long_integers():
+def test_long_integers() -> None:
# see bug-200802-systemerror
- def put(value):
+ def put(value: int) -> tuple[int, int, int, int]:
im = Image.new("RGBA", (1, 1))
im.putdata([value])
return im.getpixel((0, 0))
@@ -45,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: str) -> None:
src = hopper("L")
data = list(src.getdata())
im = Image.new(mode, src.size, 0)
@@ -67,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)
@@ -78,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: str) -> None:
data = [(16, 32, 49), (32, 32, 98)]
im = Image.new(mode, (1, 2))
im.putdata(data)
@@ -86,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
@@ -97,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
@@ -108,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 de2d90242..cc7cf58f0 100644
--- a/Tests/test_image_putpalette.py
+++ b/Tests/test_image_putpalette.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImagePalette
@@ -6,8 +7,8 @@ from PIL import Image, ImagePalette
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
-def test_putpalette():
- def palette(mode):
+def test_putpalette() -> None:
+ def palette(mode: str) -> str | tuple[str, list[int]]:
im = hopper(mode).copy()
im.putpalette(list(range(256)) * 3)
p = im.getpalette()
@@ -42,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")
@@ -56,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")
@@ -80,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: str, palette: tuple[int, ...]) -> 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 54c567aae..873a9bb5d 100644
--- a/Tests/test_image_quantize.py
+++ b/Tests/test_image_quantize.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from packaging.version import parse as parse_version
@@ -7,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"
@@ -20,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"))
@@ -32,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"
@@ -40,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)
@@ -48,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()
@@ -56,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")
@@ -66,7 +67,7 @@ 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([(p,) * 3 for p in range(0, 36, 4)])
@@ -82,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")
@@ -93,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)
@@ -119,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)
@@ -127,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 a4d0f5107..33b33d6b7 100644
--- a/Tests/test_image_reduce.py
+++ b/Tests/test_image_reduce.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageMath, ImageMode
@@ -47,7 +48,7 @@ gradients_image.load()
((1, 3), (10, 4)),
),
)
-def test_args_factor(size, expected):
+def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) -> None:
im = Image.new("L", (10, 10))
assert expected == im.reduce(size).size
@@ -55,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: float | tuple[int, int], expected_error) -> None:
im = Image.new("L", (10, 10))
with pytest.raises(expected_error):
im.reduce(size)
@@ -68,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: tuple[int, int, int, int], expected: tuple[int, int]) -> None:
im = Image.new("L", (10, 10))
assert expected == im.reduce(2, size).size
@@ -85,20 +86,20 @@ 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: str | tuple[int, int, int, int], 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: str) -> None:
im = Image.new("P", (10, 10))
with pytest.raises(ValueError):
im.reduce(3)
-def get_image(mode):
+def get_image(mode: str) -> Image.Image:
mode_info = ImageMode.getmode(mode)
if mode_info.basetype == "L":
bands = [gradients_image]
@@ -118,14 +119,19 @@ 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: Image.Image, factor: int | tuple[int, int]) -> 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: Image.Image,
+ factor: int | tuple[int, int],
+ 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
@@ -170,7 +176,9 @@ 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: Image.Image, b: Image.Image, max_average_diff: float, 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)}"
@@ -198,20 +206,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: int | tuple[int, int]) -> 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: int | tuple[int, int]) -> 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: int | tuple[int, int]) -> None:
im = get_image("LA")
# With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255))
@@ -220,27 +228,27 @@ def test_mode_LA_opaque(factor):
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_La(factor):
+def test_mode_La(factor: int | tuple[int, int]) -> 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: int | tuple[int, int]) -> 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: int | tuple[int, int]) -> 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: int | tuple[int, int]) -> None:
im = get_image("RGBA")
# With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255))
@@ -249,27 +257,27 @@ def test_mode_RGBA_opaque(factor):
@pytest.mark.parametrize("factor", remarkable_factors)
-def test_mode_RGBa(factor):
+def test_mode_RGBa(factor: int | tuple[int, int]) -> 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: int | tuple[int, int]) -> 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: int | tuple[int, int]) -> 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 b4bf6c8df..7090ff9cd 100644
--- a/Tests/test_image_resample.py
+++ b/Tests/test_image_resample.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
from contextlib import contextmanager
+from typing import Generator
import pytest
@@ -15,7 +17,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
@@ -27,7 +29,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
@@ -39,7 +41,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)
@@ -50,7 +52,7 @@ class TestImagingResampleVulnerability:
class TestImagingCoreResampleAccuracy:
- def make_case(self, mode, size, color):
+ def make_case(self, mode: str, size: tuple[int, int], color: int) -> Image.Image:
"""Makes a sample image with two dark and two bright squares.
For example:
e0 e0 1f 1f
@@ -65,7 +67,7 @@ class TestImagingCoreResampleAccuracy:
return Image.merge(mode, [case] * len(mode))
- def make_sample(self, data, size):
+ def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image:
"""Restores a sample image from given data string which contains
hex-encoded pixels from the top left fourth of a sample.
"""
@@ -82,7 +84,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: Image.Image, sample: Image.Image) -> None:
s_px = sample.load()
c_px = case.load()
for y in range(case.size[1]):
@@ -94,7 +96,7 @@ class TestImagingCoreResampleAccuracy:
)
assert s_px[x, y] == c_px[x, y], message
- def serialize_image(self, image):
+ def serialize_image(self, image: Image.Image) -> str:
s_px = image.load()
return "\n".join(
" ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0]))
@@ -102,7 +104,7 @@ class TestImagingCoreResampleAccuracy:
)
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
- def test_reduce_box(self, mode):
+ def test_reduce_box(self, mode: str) -> None:
case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off
@@ -113,7 +115,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: str) -> None:
case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off
@@ -124,7 +126,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: str) -> None:
case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off
@@ -135,7 +137,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: str) -> None:
case = self.make_case(mode, (12, 12), 0xE1)
case = case.resize((6, 6), Image.Resampling.BICUBIC)
# fmt: off
@@ -147,7 +149,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: str) -> None:
case = self.make_case(mode, (16, 16), 0xE1)
case = case.resize((8, 8), Image.Resampling.LANCZOS)
# fmt: off
@@ -160,7 +162,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: str) -> None:
case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off
@@ -171,7 +173,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: str) -> None:
case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off
@@ -182,7 +184,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: str) -> None:
case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off
@@ -193,7 +195,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: str) -> None:
case = self.make_case(mode, (4, 4), 0xE1)
case = case.resize((8, 8), Image.Resampling.BICUBIC)
# fmt: off
@@ -206,7 +208,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: str) -> None:
case = self.make_case(mode, (6, 6), 0xE1)
case = case.resize((12, 12), Image.Resampling.LANCZOS)
data = (
@@ -220,7 +222,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
)
@@ -229,11 +231,13 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency:
- def make_case(self, mode, fill):
+ def make_case(
+ self, mode: str, fill: tuple[int, int, int] | float
+ ) -> tuple[Image.Image, tuple[int, ...]]:
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: tuple[Image.Image, Image.Image]) -> None:
channel, color = case
px = channel.load()
for x in range(channel.size[0]):
@@ -242,7 +246,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]))
@@ -250,13 +254,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))
@@ -264,7 +268,7 @@ class TestCoreResampleConsistency:
class TestCoreResampleAlphaCorrect:
- def make_levels_case(self, mode):
+ def make_levels_case(self, mode: str) -> Image.Image:
i = Image.new(mode, (256, 16))
px = i.load()
for y in range(i.size[1]):
@@ -274,7 +278,7 @@ class TestCoreResampleAlphaCorrect:
px[x, y] = tuple(pix)
return i
- def run_levels_case(self, i):
+ def run_levels_case(self, i: Image.Image) -> None:
px = i.load()
for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])}
@@ -284,7 +288,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))
@@ -293,7 +297,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))
@@ -301,7 +305,9 @@ class TestCoreResampleAlphaCorrect:
self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS))
- def make_dirty_case(self, mode, clean_pixel, dirty_pixel):
+ def make_dirty_case(
+ self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...]
+ ) -> Image.Image:
i = Image.new(mode, (64, 64), dirty_pixel)
px = i.load()
xdiv4 = i.size[0] // 4
@@ -311,7 +317,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: Image.Image, clean_pixel: tuple[int, ...]) -> None:
px = i.load()
for y in range(i.size[1]):
for x in range(i.size[0]):
@@ -322,7 +328,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(
@@ -338,7 +344,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,))
@@ -349,27 +355,27 @@ class TestCoreResampleAlphaCorrect:
class TestCoreResamplePasses:
@contextmanager
- def count(self, diff):
+ def count(self, diff: int) -> Generator[None, None, None]:
count = Image.core.get_stats()["new_count"]
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):
@@ -379,7 +385,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):
@@ -391,7 +397,7 @@ class TestCoreResamplePasses:
class TestCoreResampleCoefficients:
- def test_reduce(self):
+ def test_reduce(self) -> None:
test_color = 254
for size in range(400000, 400010, 2):
@@ -403,7 +409,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))
@@ -431,7 +437,7 @@ class TestCoreResampleBox:
Image.Resampling.LANCZOS,
),
)
- def test_wrong_arguments(self, resample):
+ def test_wrong_arguments(self, resample: Image.Resampling) -> 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))
@@ -458,8 +464,12 @@ class TestCoreResampleBox:
with pytest.raises(ValueError, match="can't exceed"):
im.resize((32, 32), resample, (0, 0, im.width, im.height + 1))
- def resize_tiled(self, im, dst_size, xtiles, ytiles):
- def split_range(size, tiles):
+ def resize_tiled(
+ self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int
+ ) -> Image.Image:
+ def split_range(
+ size: int, tiles: int
+ ) -> Generator[tuple[int, int], None, None]:
scale = size / tiles
for i in range(tiles):
yield int(round(scale * i)), int(round(scale * (i + 1)))
@@ -477,7 +487,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)
@@ -490,7 +500,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:
@@ -517,14 +527,14 @@ class TestCoreResampleBox:
@pytest.mark.parametrize(
"resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR)
)
- def test_formats(self, mode, resample):
+ def test_formats(self, mode: str, resample: Image.Resampling) -> 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()
@@ -538,7 +548,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()
@@ -557,7 +567,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: Image.Resampling) -> None:
# Can skip resize for one dimension
im = hopper()
@@ -580,7 +590,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: Image.Resampling) -> None:
# Can skip resize for one dimension
im = hopper()
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index 0d3b43ee2..a64e4a846 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -1,8 +1,12 @@
"""
Tests for resize functionality.
"""
+
from __future__ import annotations
+
from itertools import permutations
+from pathlib import Path
+from typing import Generator
import pytest
@@ -18,7 +22,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))
@@ -26,14 +32,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):
@@ -58,7 +64,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)
@@ -74,7 +80,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)
@@ -98,7 +104,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.
@@ -138,17 +146,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.
@@ -161,7 +169,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:
@@ -171,7 +179,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
)
@@ -190,7 +198,12 @@ 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
@@ -205,7 +218,12 @@ 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
@@ -220,7 +238,12 @@ 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
@@ -232,7 +255,9 @@ class TestReducingGapResize:
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
@@ -244,7 +269,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
@@ -254,8 +284,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
@@ -270,7 +300,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:
@@ -278,13 +308,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 0931aa32d..c10c96da6 100644
--- a/Tests/test_image_rotate.py
+++ b/Tests/test_image_rotate.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -11,7 +12,13 @@ from .helper import (
)
-def rotate(im, mode, angle, center=None, translate=None):
+def rotate(
+ im: Image.Image,
+ mode: str,
+ angle: int,
+ center: tuple[int, int] | None = None,
+ translate: tuple[int, int] | None = None,
+) -> None:
out = im.rotate(angle, center=center, translate=translate)
assert out.mode == mode
assert out.size == im.size # default rotate clips output
@@ -26,13 +33,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: str) -> None:
im = hopper(mode)
rotate(im, mode, 45)
@pytest.mark.parametrize("angle", (0, 90, 180, 270))
-def test_angle(angle):
+def test_angle(angle: int) -> None:
with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle)
@@ -41,12 +48,12 @@ def test_angle(angle):
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
-def test_zero(angle):
+def test_zero(angle: int) -> 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)
@@ -63,7 +70,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)
@@ -74,7 +81,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)
@@ -85,7 +92,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
@@ -98,7 +105,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):
@@ -106,7 +113,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):
@@ -114,26 +121,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)
@@ -141,7 +148,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 707508250..3385f81f5 100644
--- a/Tests/test_image_split.py
+++ b/Tests/test_image_split.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, features
@@ -6,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]
@@ -35,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 9e6796ca2..2ca1d2cfc 100644
--- a/Tests/test_image_thumbnail.py
+++ b/Tests/test_image_thumbnail.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -13,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)
@@ -66,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))
@@ -91,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:
@@ -105,12 +106,12 @@ 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
- def im_draft(mode, size):
+ def im_draft(mode: str, size: tuple[int, int]):
result = draft(mode, size)
assert result is not None
@@ -123,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))
@@ -137,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)
@@ -154,7 +155,7 @@ def test_reducing_gap_values():
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 156b9919d..f7a3cc41d 100644
--- a/Tests/test_image_tobitmap.py
+++ b/Tests/test_image_tobitmap.py
@@ -1,10 +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 f6042bca5..d32b6c09b 100644
--- a/Tests/test_image_tobytes.py
+++ b/Tests/test_image_tobytes.py
@@ -1,7 +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 15939ef64..638d12247 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
import math
+from typing import Callable
import pytest
@@ -9,21 +11,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:
@@ -33,14 +42,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(
@@ -55,7 +64,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
@@ -83,7 +92,7 @@ class TestImageTransform:
("LA", (76, 0)),
),
)
- def test_fill(self, mode, expected_pixel):
+ def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None:
im = hopper(mode)
(w, h) = im.size
transformed = im.transform(
@@ -95,7 +104,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
@@ -134,7 +143,9 @@ 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: Callable[[Image.Image, tuple[int, int]], Image.Image]
+ ) -> None:
# create image with half white, half black,
# with the black half transparent.
# do op,
@@ -150,14 +161,14 @@ class TestImageTransform:
hist = im_background.histogram()
assert 40 * 10 == hist[-1]
- def test_alpha_premult_resize(self):
- def op(im, sz):
+ def test_alpha_premult_resize(self) -> None:
+ def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
return im.resize(sz, Image.Resampling.BILINEAR)
self._test_alpha_premult(op)
- def test_alpha_premult_transform(self):
- def op(im, sz):
+ def test_alpha_premult_transform(self) -> None:
+ def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
(w, h) = im.size
return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
@@ -165,7 +176,9 @@ class TestImageTransform:
self._test_alpha_premult(op)
- def _test_nearest(self, op, mode):
+ def _test_nearest(
+ self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str
+ ) -> None:
# create white image with half transparent,
# do op,
# the image should remain white with half transparent
@@ -188,15 +201,15 @@ class TestImageTransform:
)
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
- def test_nearest_resize(self, mode):
- def op(im, sz):
+ def test_nearest_resize(self, mode: str) -> None:
+ def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
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 op(im, sz):
+ def test_nearest_transform(self, mode: str) -> None:
+ def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
(w, h) = im.size
return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST
@@ -204,7 +217,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
#
@@ -219,20 +232,22 @@ class TestImageTransform:
# Running by default, but I'd totally understand not doing it in
# the future
- pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)]
+ pattern: list[Image.Image] | None = [
+ Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)
+ ]
# Yeah. Watch some JIT optimize this out.
pattern = None # noqa: F841
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: Image.Resampling | str) -> None:
with hopper() as im:
(w, h) = im.size
with pytest.raises(ValueError):
@@ -242,7 +257,7 @@ class TestImageTransform:
class TestImageTransformAffine:
transform = Image.Transform.AFFINE
- def _test_image(self):
+ def _test_image(self) -> Image.Image:
im = hopper("RGB")
return im.crop((10, 20, im.width - 10, im.height - 20))
@@ -255,7 +270,7 @@ class TestImageTransformAffine:
(270, Image.Transpose.ROTATE_270),
),
)
- def test_rotate(self, deg, transpose):
+ def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None:
im = self._test_image()
angle = -math.radians(deg)
@@ -305,7 +320,13 @@ class TestImageTransformAffine:
(Image.Resampling.BICUBIC, 1),
),
)
- def test_resize(self, scale, epsilon_scale, resample, epsilon):
+ def test_resize(
+ self,
+ scale: float,
+ epsilon_scale: float,
+ resample: Image.Resampling,
+ epsilon: int,
+ ) -> None:
im = self._test_image()
size_up = int(round(im.width * scale)), int(round(im.height * scale))
@@ -334,7 +355,14 @@ class TestImageTransformAffine:
(Image.Resampling.BICUBIC, 1),
),
)
- def test_translate(self, x, y, epsilon_scale, resample, epsilon):
+ def test_translate(
+ self,
+ x: float,
+ y: float,
+ epsilon_scale: float,
+ resample: Image.Resampling,
+ epsilon: float,
+ ) -> 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 66a2d9e29..d384d8141 100644
--- a/Tests/test_image_transpose.py
+++ b/Tests/test_image_transpose.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+
import pytest
+from PIL import Image
from PIL.Image import Transpose
from . import helper
@@ -13,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
@@ -27,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
@@ -41,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
@@ -55,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
@@ -69,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
@@ -83,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
@@ -97,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
@@ -111,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 8e3a738d7..7e2290c15 100644
--- a/Tests/test_imagechops.py
+++ b/Tests/test_imagechops.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from typing import Callable
+
from PIL import Image, ImageChops
from .helper import assert_image_equal, hopper
@@ -14,7 +17,7 @@ WHITE = (255, 255, 255)
GRAY = 128
-def test_sanity():
+def test_sanity() -> None:
im = hopper("L")
ImageChops.constant(im, 128)
@@ -47,7 +50,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:
@@ -59,7 +62,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:
@@ -71,7 +74,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()
@@ -82,7 +85,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:
@@ -94,7 +97,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()
@@ -105,7 +108,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:
@@ -117,7 +120,7 @@ def test_blend():
assert new.getpixel((50, 50)) == BROWN
-def test_constant():
+def test_constant() -> None:
# Arrange
im = Image.new("RGB", (20, 10))
@@ -130,7 +133,7 @@ def test_constant():
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:
@@ -141,7 +144,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:
@@ -152,7 +155,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:
@@ -163,7 +166,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:
@@ -174,7 +177,7 @@ def test_difference_pixel():
assert new.getpixel((50, 50)) == (240, 166, 128)
-def test_duplicate():
+def test_duplicate() -> None:
# Arrange
im = hopper()
@@ -185,7 +188,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
@@ -197,7 +200,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:
@@ -208,7 +211,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:
@@ -219,7 +222,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
@@ -233,7 +236,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")
@@ -247,7 +250,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()
@@ -260,7 +263,7 @@ def test_multiply_white():
assert_image_equal(new, im1)
-def test_offset():
+def test_offset() -> None:
# Arrange
xoffset = 45
yoffset = 20
@@ -277,7 +280,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:
@@ -289,7 +292,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:
@@ -302,7 +305,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:
@@ -314,7 +317,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:
@@ -325,7 +328,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:
@@ -338,7 +341,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:
@@ -349,7 +352,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:
@@ -361,7 +364,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:
@@ -373,7 +376,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:
@@ -385,8 +388,10 @@ def test_overlay():
assert new.getpixel((15, 100)) == (1, 1, 2)
-def test_logical():
- def table(op, a, b):
+def test_logical() -> None:
+ def table(
+ op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
+ ) -> tuple[int, int, int, int]:
out = []
for x in (a, b):
imx = Image.new("1", (1, 1), x)
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index 0dde82bd7..6be29a70f 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -1,9 +1,12 @@
from __future__ import annotations
+
import datetime
import os
import re
import shutil
from io import BytesIO
+from pathlib import Path
+from typing import Any
import pytest
@@ -31,7 +34,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
@@ -41,16 +44,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]
@@ -90,7 +93,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 (
@@ -99,7 +112,7 @@ def test_name():
)
-def test_info():
+def test_info() -> None:
skip_missing()
assert ImageCms.getProfileInfo(SRGB).splitlines() == [
"sRGB IEC61966-2-1 black scaled",
@@ -109,7 +122,7 @@ def test_info():
]
-def test_copyright():
+def test_copyright() -> None:
skip_missing()
assert (
ImageCms.getProfileCopyright(SRGB).strip()
@@ -117,12 +130,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()
@@ -130,14 +143,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(
@@ -146,7 +159,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)"
@@ -159,7 +172,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:
@@ -170,7 +183,7 @@ def test_extensions():
)
-def test_exceptions():
+def test_exceptions() -> None:
# Test mode mismatch
psRGB = ImageCms.createProfile("sRGB")
pLab = ImageCms.createProfile("LAB")
@@ -196,17 +209,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(
@@ -216,7 +229,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',
@@ -225,7 +238,7 @@ def test_invalid_color_temperature():
@pytest.mark.parametrize("flag", ("my string", -1))
-def test_invalid_flag(flag):
+def test_invalid_flag(flag: str | int) -> None:
with hopper() as im:
with pytest.raises(
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
@@ -233,7 +246,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")
@@ -257,7 +270,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")
@@ -272,7 +285,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")
@@ -289,7 +302,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")
@@ -306,7 +319,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"]))
@@ -318,22 +331,26 @@ 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: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10
+ ) -> None:
# Helper function to reduce precision of tuples of floats
# recursively and then check equality.
power = 10**digits
- def truncate_tuple(tuple_or_float):
+ def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]:
return tuple(
- truncate_tuple(val)
- if isinstance(val, tuple)
- else int(val * power) / power
- for val in tuple_or_float
+ (
+ truncate_tuple(val)
+ if isinstance(val, tuple)
+ else int(val * power) / power
+ )
+ for val in tuple_value
)
assert truncate_tuple(tup1) == truncate_tuple(tup2)
@@ -465,7 +482,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:
@@ -478,7 +495,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
@@ -490,8 +507,10 @@ def test_profile_typesafety():
ImageCms.ImageCmsProfile(1).tobytes()
-def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel):
- def create_test_image():
+def assert_aux_channel_preserved(
+ mode: str, transform_in_place: bool, preserved_channel: str
+) -> None:
+ def create_test_image() -> Image.Image:
# set up test image with something interesting in the tested aux channel.
# fmt: off
nine_grid_deltas = [
@@ -545,31 +564,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
@@ -619,7 +638,7 @@ def test_auxiliary_channels_isolated():
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
-def test_rgb_lab(mode):
+def test_rgb_lab(mode: str) -> None:
im = Image.new(mode, (1, 1))
converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
@@ -627,3 +646,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 c0ffd2ebf..6eea7886d 100644
--- a/Tests/test_imagecolor.py
+++ b/Tests/test_imagecolor.py
@@ -1,10 +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")
@@ -56,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")
@@ -65,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)")
@@ -159,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")
@@ -194,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 379fe78cd..f7aea3034 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1,10 +1,12 @@
from __future__ import annotations
+
import contextlib
import os.path
import pytest
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
+from PIL._typing import Coords
from .helper import (
assert_image_equal,
@@ -46,7 +48,7 @@ KITE_POINTS = (
)
-def test_sanity():
+def test_sanity() -> None:
im = hopper("RGB").copy()
draw = ImageDraw.ImageDraw(im)
@@ -58,13 +60,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):
@@ -73,7 +75,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: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -86,7 +88,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: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -101,7 +103,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: Coords) -> None:
# No need to go in loops
# Arrange
im = Image.new("RGB", (W, H))
@@ -117,7 +119,7 @@ def test_arc_no_loops(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_arc_width(bbox):
+def test_arc_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -130,7 +132,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: Coords) -> None:
# Tests an arc with a large enough width that it is a pieslice
# Arrange
im = Image.new("RGB", (W, H))
@@ -144,7 +146,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: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -157,7 +159,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: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -170,7 +172,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)
@@ -183,7 +185,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)
@@ -199,7 +201,7 @@ def test_bitmap():
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", BBOX)
-def test_chord(mode, bbox):
+def test_chord(mode: str, bbox: Coords) -> None:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@@ -213,7 +215,7 @@ def test_chord(mode, bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_chord_width(bbox):
+def test_chord_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -226,7 +228,7 @@ def test_chord_width(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_chord_width_fill(bbox):
+def test_chord_width_fill(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -239,7 +241,7 @@ def test_chord_width_fill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_chord_zero_width(bbox):
+def test_chord_zero_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -251,7 +253,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)
@@ -265,7 +267,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: str, bbox: Coords) -> None:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@@ -279,7 +281,7 @@ def test_ellipse(mode, bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse_translucent(bbox):
+def test_ellipse_translucent(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
@@ -292,7 +294,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)
@@ -304,7 +306,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)),
@@ -316,7 +318,7 @@ def test_ellipse_symmetric():
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse_width(bbox):
+def test_ellipse_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -328,7 +330,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)
@@ -341,7 +343,7 @@ def test_ellipse_width_large():
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse_width_fill(bbox):
+def test_ellipse_width_fill(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -354,7 +356,7 @@ def test_ellipse_width_fill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse_zero_width(bbox):
+def test_ellipse_zero_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -366,7 +368,7 @@ def test_ellipse_zero_width(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png")
-def ellipse_various_sizes_helper(filled):
+def ellipse_various_sizes_helper(filled: bool) -> Image.Image:
ellipse_sizes = range(32)
image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1
im = Image.new("RGB", (image_size, image_size))
@@ -393,13 +395,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(
@@ -408,7 +410,7 @@ def test_ellipse_various_sizes_filled():
@pytest.mark.parametrize("points", POINTS)
-def test_line(points):
+def test_line(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -420,7 +422,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)
@@ -441,7 +443,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)
@@ -462,7 +464,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()
@@ -481,7 +483,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: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -494,7 +496,7 @@ def test_pieslice(bbox, start, end):
@pytest.mark.parametrize("bbox", BBOX)
-def test_pieslice_width(bbox):
+def test_pieslice_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -507,7 +509,7 @@ def test_pieslice_width(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_pieslice_width_fill(bbox):
+def test_pieslice_width_fill(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -521,7 +523,7 @@ def test_pieslice_width_fill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_pieslice_zero_width(bbox):
+def test_pieslice_zero_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -533,7 +535,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)
@@ -545,7 +547,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 = (
@@ -576,7 +578,7 @@ def test_pieslice_no_spikes():
@pytest.mark.parametrize("points", POINTS)
-def test_point(points):
+def test_point(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -588,7 +590,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)
@@ -601,7 +603,7 @@ def test_point_I16():
@pytest.mark.parametrize("points", POINTS)
-def test_polygon(points):
+def test_polygon(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -615,7 +617,9 @@ 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: str, kite_points: tuple[tuple[int, int], ...] | list[tuple[int, int]]
+) -> None:
# Test drawing lines of different gradients (dx>dy, dy>dx) and
# vertical (dx==0) and horizontal (dy==0) lines
# Arrange
@@ -630,7 +634,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))
@@ -644,7 +648,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))
@@ -658,7 +662,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")
@@ -672,7 +676,7 @@ def test_polygon_translucent():
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle(bbox):
+def test_rectangle(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -684,7 +688,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))
@@ -699,7 +703,7 @@ def test_big_rectangle():
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_width(bbox):
+def test_rectangle_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -713,7 +717,7 @@ def test_rectangle_width(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_width_fill(bbox):
+def test_rectangle_width_fill(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -727,7 +731,7 @@ def test_rectangle_width_fill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_zero_width(bbox):
+def test_rectangle_zero_width(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -740,7 +744,7 @@ def test_rectangle_zero_width(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_I16(bbox):
+def test_rectangle_I16(bbox: Coords) -> None:
# Arrange
im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im)
@@ -753,7 +757,7 @@ def test_rectangle_I16(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle_translucent_outline(bbox):
+def test_rectangle_translucent_outline(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
@@ -771,7 +775,13 @@ 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: (
+ tuple[int, int, int, int]
+ | tuple[list[int]]
+ | tuple[tuple[int, int], tuple[int, int]]
+ )
+) -> None:
# Arrange
im = Image.new("RGB", (200, 200))
draw = ImageDraw.Draw(im)
@@ -787,7 +797,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: bool, top_right: bool, bottom_right: bool, bottom_left: bool
+) -> None:
corners = (top_left, top_right, bottom_right, bottom_left)
# Arrange
@@ -821,7 +833,9 @@ 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: tuple[int, int, int, int], radius: float, type: str
+) -> None:
# Arrange
im = Image.new("RGB", (200, 200))
draw = ImageDraw.Draw(im)
@@ -837,7 +851,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: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -859,7 +873,9 @@ 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: tuple[int, int, int, int], suffix: str
+) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
@@ -876,7 +892,7 @@ def test_rounded_rectangle_translucent(xy, suffix):
@pytest.mark.parametrize("bbox", BBOX)
-def test_floodfill(bbox):
+def test_floodfill(bbox: Coords) -> None:
red = ImageColor.getrgb("red")
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
@@ -909,7 +925,7 @@ def test_floodfill(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_floodfill_border(bbox):
+def test_floodfill_border(bbox: Coords) -> None:
# floodfill() is experimental
# Arrange
@@ -931,7 +947,7 @@ def test_floodfill_border(bbox):
@pytest.mark.parametrize("bbox", BBOX)
-def test_floodfill_thresh(bbox):
+def test_floodfill_thresh(bbox: Coords) -> None:
# floodfill() is experimental
# Arrange
@@ -947,7 +963,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
@@ -965,8 +981,11 @@ def test_floodfill_not_negative():
def create_base_image_draw(
- size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY
-):
+ size: tuple[int, int],
+ mode: str = DEFAULT_MODE,
+ background1: tuple[int, int, int] = WHITE,
+ background2: tuple[int, int, int] = GRAY,
+) -> tuple[Image.Image, ImageDraw.ImageDraw]:
img = Image.new(mode, size, background1)
for x in range(0, size[0]):
for y in range(0, size[1]):
@@ -975,7 +994,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)
@@ -988,7 +1007,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(
@@ -1000,7 +1019,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: tuple[int, int, int] | None, suffix: str) -> 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(
@@ -1008,7 +1027,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(
@@ -1046,7 +1065,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)
@@ -1057,7 +1076,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(
@@ -1103,7 +1122,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)
@@ -1125,7 +1144,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))
@@ -1138,7 +1157,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)
@@ -1232,7 +1251,7 @@ def test_wide_line_larger_than_int():
],
],
)
-def test_line_joint(xy):
+def test_line_joint(xy: list[tuple[int, int]] | tuple[int, ...] | list[int]) -> None:
im = Image.new("RGB", (500, 325))
draw = ImageDraw.Draw(im)
@@ -1243,7 +1262,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))
@@ -1259,7 +1278,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)
@@ -1273,7 +1292,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))
@@ -1290,7 +1309,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)
@@ -1304,7 +1323,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()
@@ -1325,7 +1344,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)
@@ -1341,7 +1360,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)
@@ -1358,7 +1377,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."
@@ -1385,7 +1404,7 @@ def test_default_font_size():
@pytest.mark.parametrize("bbox", BBOX)
-def test_same_color_outline(bbox):
+def test_same_color_outline(bbox: Coords) -> None:
# Prepare shape
x0, y0 = 5, 5
x1, y1 = 5, 50
@@ -1399,7 +1418,8 @@ def test_same_color_outline(bbox):
# Begin
for mode in ["RGB", "L"]:
- for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]:
+ fill = "red"
+ for outline in [None, "red", "#f00"]:
for operation, args in {
"chord": [bbox, 0, 180],
"ellipse": [bbox],
@@ -1414,6 +1434,7 @@ def test_same_color_outline(bbox):
# Act
draw_method = getattr(draw, operation)
+ assert isinstance(args, list)
args += [fill, outline]
draw_method(*args)
@@ -1431,7 +1452,9 @@ 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: int, polygon_name: str, args: dict[str, int | str]
+) -> 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)
@@ -1468,7 +1491,9 @@ 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: int, expected_vertices: list[tuple[float, float]]
+) -> None:
bounding_circle = (W // 2, H // 2, 25)
vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0)
assert vertices == expected_vertices
@@ -1479,7 +1504,7 @@ def test_compute_regular_polygon_vertices(n_sides, expected_vertices):
[
(None, (50, 50, 25), 0, TypeError, "n_sides should be an int"),
(1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"),
- (3, 50, 0, TypeError, "bounding_circle should be a tuple"),
+ (3, 50, 0, TypeError, "bounding_circle should be a sequence"),
(
3,
(50, 50, 100, 100),
@@ -1520,13 +1545,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),
@@ -1545,7 +1570,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)
@@ -1557,7 +1582,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")
@@ -1566,7 +1591,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: tuple[int, int, int, int]) -> 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 d729af14d..3171eb9ae 100644
--- a/Tests/test_imagedraw2.py
+++ b/Tests/test_imagedraw2.py
@@ -1,9 +1,11 @@
from __future__ import annotations
+
import os.path
import pytest
from PIL import Image, ImageDraw, ImageDraw2, features
+from PIL._typing import Coords
from .helper import (
assert_image_equal,
@@ -42,7 +44,7 @@ POINTS = (
FONT_PATH = "Tests/fonts/FreeMono.ttf"
-def test_sanity():
+def test_sanity() -> None:
im = hopper("RGB").copy()
draw = ImageDraw2.Draw(im)
@@ -55,7 +57,7 @@ def test_sanity():
@pytest.mark.parametrize("bbox", BBOX)
-def test_ellipse(bbox):
+def test_ellipse(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -69,7 +71,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)
@@ -83,7 +85,7 @@ def test_ellipse_edge():
@pytest.mark.parametrize("points", POINTS)
-def test_line(points):
+def test_line(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -97,7 +99,7 @@ def test_line(points):
@pytest.mark.parametrize("points", POINTS)
-def test_line_pen_as_brush(points):
+def test_line_pen_as_brush(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -113,7 +115,7 @@ def test_line_pen_as_brush(points):
@pytest.mark.parametrize("points", POINTS)
-def test_polygon(points):
+def test_polygon(points: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -128,7 +130,7 @@ def test_polygon(points):
@pytest.mark.parametrize("bbox", BBOX)
-def test_rectangle(bbox):
+def test_rectangle(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -142,7 +144,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))
@@ -159,7 +161,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)
@@ -174,7 +176,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)
@@ -189,7 +191,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)
@@ -205,7 +207,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 f4e4d59be..6ebc61e1b 100644
--- a/Tests/test_imageenhance.py
+++ b/Tests/test_imageenhance.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageEnhance
@@ -6,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)
@@ -15,13 +16,13 @@ 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)
-def _half_transparent_image():
+def _half_transparent_image() -> Image.Image:
# returns an image, half transparent, half solid
im = hopper("RGB")
@@ -33,7 +34,9 @@ def _half_transparent_image():
return im
-def _check_alpha(im, original, op, amount):
+def _check_alpha(
+ im: Image.Image, original: Image.Image, op: str, amount: float
+) -> None:
assert im.getbands() == original.getbands()
assert_image_equal(
im.getchannel("A"),
@@ -43,7 +46,7 @@ def _check_alpha(im, original, op, amount):
@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
-def test_alpha(op):
+def test_alpha(op: str) -> 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 4804a554f..44521a8b3 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -29,8 +30,8 @@ SAFEBLOCK = ImageFile.SAFEBLOCK
class TestImageFile:
- def test_parser(self):
- def roundtrip(format):
+ def test_parser(self) -> None:
+ def roundtrip(format: str) -> tuple[Image.Image, Image.Image]:
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
if format in ("MSP", "XBM"):
im = im.convert("1")
@@ -83,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:
@@ -92,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))
@@ -104,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:
@@ -115,17 +116,17 @@ class TestImageFile:
assert_image_equal(im1, im2)
- def test_raise_oserror(self):
+ 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()
@@ -133,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)
@@ -145,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(
@@ -165,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()
@@ -175,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:
@@ -184,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:
@@ -209,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
@@ -217,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)
@@ -226,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)
@@ -243,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)
@@ -258,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)
@@ -271,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)
@@ -284,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)
@@ -297,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)
@@ -319,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)
@@ -333,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)
@@ -351,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)
@@ -371,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)
@@ -387,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 6ad56e2b1..05b5d4716 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import copy
import os
import re
@@ -6,11 +7,13 @@ import shutil
import sys
from io import BytesIO
from pathlib import Path
+from typing import Any, BinaryIO
import pytest
from packaging.version import parse as parse_version
from PIL import Image, ImageDraw, ImageFont, features
+from PIL._typing import StrOrBytesPath
from .helper import (
assert_image_equal,
@@ -30,7 +33,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"))
@@ -41,16 +44,16 @@ def test_sanity():
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
],
)
-def layout_engine(request):
+def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
return request.param
@pytest.fixture(scope="module")
-def font(layout_engine):
+def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont:
return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine)
-def test_font_properties(font):
+def test_font_properties(font: ImageFont.FreeTypeFont) -> None:
assert font.path == FONT_PATH
assert font.size == FONT_SIZE
@@ -66,7 +69,9 @@ def test_font_properties(font):
assert font_copy.path == second_font_path
-def _render(font, layout_engine):
+def _render(
+ font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout
+) -> Image.Image:
txt = "Hello World!"
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine)
ttf.getbbox(txt)
@@ -79,12 +84,12 @@ def _render(font, layout_engine):
@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
-def test_font_with_name(layout_engine, font):
+def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None:
_render(font, layout_engine)
-def test_font_with_filelike(layout_engine):
- def _font_as_bytes():
+def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None:
+ def _font_as_bytes() -> BytesIO:
with open(FONT_PATH, "rb") as f:
font_bytes = BytesIO(f.read())
return font_bytes
@@ -101,12 +106,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: ImageFont.Layout) -> None:
with open(FONT_PATH, "rb") as f:
_render(f, layout_engine)
-def test_render_equal(layout_engine):
+def test_render_equal(layout_engine: ImageFont.Layout) -> None:
img_path = _render(FONT_PATH, layout_engine)
with open(FONT_PATH, "rb") as f:
font_filelike = BytesIO(f.read())
@@ -115,7 +120,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: ImageFont.Layout) -> None:
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
try:
shutil.copy(FONT_PATH, tempfile)
@@ -125,7 +130,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: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGBA", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -139,7 +144,7 @@ def test_transparent_background(font):
assert_image_similar_tofile(im.convert("L"), target, 0.01)
-def test_I16(font):
+def test_I16(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="I;16", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -152,7 +157,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: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -180,8 +185,14 @@ def test_textbbox_equal(font):
),
)
def test_getlength(
- text, mode, fontname, size, layout_engine, length_basic, length_raqm
-):
+ text: str,
+ mode: str,
+ fontname: str,
+ size: int,
+ layout_engine: ImageFont.Layout,
+ length_basic: int,
+ length_raqm: float,
+) -> None:
f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
im = Image.new(mode, (1, 1), 0)
@@ -196,7 +207,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(
@@ -206,7 +217,7 @@ def test_float_size():
assert lengths[0] != lengths[1] != lengths[2]
-def test_render_multiline(font):
+def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
line_spacing = font.getbbox("A")[3] + 4
@@ -222,7 +233,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: ImageFont.FreeTypeFont) -> None:
# Test that text() correctly connects to multiline_text()
# and that align defaults to left
im = Image.new(mode="RGB", size=(300, 100))
@@ -242,7 +253,9 @@ 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: ImageFont.FreeTypeFont, align: str, ext: str
+) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align)
@@ -250,7 +263,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: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -259,14 +272,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: ImageFont.FreeTypeFont) -> 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: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -284,7 +297,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: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -294,7 +307,7 @@ def test_multiline_width(font):
)
-def test_multiline_spacing(font):
+def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None:
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10)
@@ -305,7 +318,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):
+def test_rotated_transposed_font(
+ font: ImageFont.FreeTypeFont, orientation: Image.Transpose
+) -> None:
img_gray = Image.new("L", (100, 100))
draw = ImageDraw.Draw(img_gray)
word = "testing"
@@ -346,7 +361,9 @@ def test_rotated_transposed_font(font, orientation):
Image.Transpose.FLIP_TOP_BOTTOM,
),
)
-def test_unrotated_transposed_font(font, orientation):
+def test_unrotated_transposed_font(
+ font: ImageFont.FreeTypeFont, orientation: Image.Transpose
+) -> None:
img_gray = Image.new("L", (100, 100))
draw = ImageDraw.Draw(img_gray)
word = "testing"
@@ -381,7 +398,9 @@ 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: ImageFont.FreeTypeFont, orientation: Image.Transpose
+) -> None:
# Arrange
text = "mask this"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -402,7 +421,9 @@ 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: ImageFont.FreeTypeFont, orientation: Image.Transpose
+) -> None:
# Arrange
text = "mask this"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@@ -414,11 +435,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: ImageFont.FreeTypeFont) -> None:
assert ("FreeMono", "Regular") == font.getname()
-def test_free_type_font_get_metrics(font):
+def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None:
ascent, descent = font.getmetrics()
assert isinstance(ascent, int)
@@ -426,7 +447,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: ImageFont.FreeTypeFont) -> None:
# Arrange
text = "mask this"
@@ -437,7 +458,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"
@@ -448,13 +469,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))
@@ -472,16 +493,16 @@ def test_default_font():
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
-def test_getbbox(font, mode):
+def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
assert (0, 4, 12, 16) == font.getbbox("A", mode)
-def test_getbbox_empty(font):
+def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None:
# issue #2614, should not crash.
assert (0, 0, 0, 0) == font.getbbox("")
-def test_render_empty(font):
+def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
# issue 2666
im = Image.new(mode="RGB", size=(300, 100))
target = im.copy()
@@ -491,7 +512,7 @@ def test_render_empty(font):
assert_image_equal(im, target)
-def test_unicode_extended(layout_engine):
+def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
# issue #3777
text = "A\u278A\U0001F12B"
target = "Tests/images/unicode_extended.png"
@@ -514,21 +535,23 @@ 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: pytest.MonkeyPatch, platform: str, font_directory: str
+) -> None:
+ def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None:
# Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
with monkeypatch.context() as m:
m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
- def loadable_font(filepath, size, index, encoding, *args, **kwargs):
+ def loadable_font(
+ filepath: str, size: int, index: int, encoding: str, *args: Any
+ ):
if filepath == path_to_fake:
return ImageFont._FreeTypeFont(
- FONT_PATH, size, index, encoding, *args, **kwargs
+ FONT_PATH, size, index, encoding, *args
)
- return ImageFont._FreeTypeFont(
- filepath, size, index, encoding, *args, **kwargs
- )
+ return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args)
m.setattr(ImageFont, "FreeTypeFont", loadable_font)
font = ImageFont.truetype(fontname)
@@ -542,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory):
if platform == "linux":
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
- def fake_walker(path):
+ def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
if path == font_directory:
return [
(
@@ -566,7 +589,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: ImageFont.FreeTypeFont) -> None:
assert font.getmetrics() == (16, 4)
assert font.font.ascent == 16
assert font.font.descent == 4
@@ -587,7 +610,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: ImageFont.FreeTypeFont, stroke_width: int) -> None:
assert font.getbbox("A", stroke_width=stroke_width) == (
0 - stroke_width,
4 - stroke_width,
@@ -596,7 +619,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")
@@ -606,7 +629,7 @@ def test_complex_font_settings():
t.getmask("абвг", language="sr")
-def test_variation_get(font):
+def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
@@ -661,7 +684,7 @@ def test_variation_get(font):
]
-def _check_text(font, path, epsilon):
+def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None:
im = Image.new("RGB", (100, 75), "white")
d = ImageDraw.Draw(im)
d.text((10, 10), "Text", font=font, fill="black")
@@ -676,7 +699,7 @@ def _check_text(font, path, epsilon):
raise
-def test_variation_set_by_name(font):
+def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
@@ -701,7 +724,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: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2"))
if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError):
@@ -736,7 +759,9 @@ 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: ImageFont.Layout, anchor: str, left: int, top: int
+) -> None:
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
@@ -781,7 +806,9 @@ def test_anchor(layout_engine, anchor, left, top):
("md", "center"),
),
)
-def test_anchor_multiline(layout_engine, anchor, align):
+def test_anchor_multiline(
+ layout_engine: ImageFont.Layout, anchor: str, align: str
+) -> None:
target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
text = "a\nlong\ntext sample"
@@ -799,7 +826,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: ImageFont.FreeTypeFont) -> None:
im = Image.new("RGB", (100, 100), "white")
d = ImageDraw.Draw(im)
d.font = font
@@ -825,7 +852,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: ImageFont.Layout, bpp: int) -> None:
text = "Bitmap Font"
layout_name = ["basic", "raqm"][layout_engine]
target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
@@ -842,7 +869,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: ImageFont.Layout) -> None:
text = "Bitmap Font"
layout_name = ["basic", "raqm"][layout_engine]
target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
@@ -859,7 +886,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: ImageFont.Layout, embedded_color: bool) -> 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: ImageFont.Layout) -> None:
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
ttf.getbbox(txt)
@@ -872,7 +912,7 @@ def test_standard_embedded_color(layout_engine):
@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
-def test_float_coord(layout_engine, fontmode):
+def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None:
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
@@ -894,43 +934,41 @@ def test_float_coord(layout_engine, fontmode):
raise
-def test_cbdt(layout_engine):
+def test_cbdt(layout_engine: ImageFont.Layout) -> 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: ImageFont.Layout) -> 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: ImageFont.Layout) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
@@ -947,7 +985,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: ImageFont.Layout) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
@@ -965,7 +1003,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: ImageFont.Layout) -> None:
font = ImageFont.truetype(
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
size=64,
@@ -981,7 +1019,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: ImageFont.Layout) -> None:
font = ImageFont.truetype(
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
size=64,
@@ -996,7 +1034,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: ImageFont.Layout) -> None:
try:
font = ImageFont.truetype(
"Tests/fonts/OpenSans.woff2",
@@ -1015,7 +1053,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")
@@ -1030,7 +1068,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: ImageFont.FreeTypeFont) -> None:
with pytest.raises(ValueError):
font.getlength("A" * 1_000_001)
with pytest.raises(ValueError):
@@ -1042,11 +1080,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(
@@ -1056,14 +1096,14 @@ def test_too_many_characters(font):
"Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
],
)
-def test_oom(test_file):
+def test_oom(test_file: str) -> 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: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
with pytest.warns(UserWarning) as record:
font = ImageFont.truetype(
@@ -1077,6 +1117,8 @@ def test_raqm_missing_warning(monkeypatch):
@pytest.mark.parametrize("size", [-1, 0])
-def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size):
+def test_invalid_truetype_sizes_raise_valueerror(
+ layout_engine: ImageFont.Layout, size: int
+) -> 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 bea532b05..24c7b871a 100644
--- a/Tests/test_imagefontctl.py
+++ b/Tests/test_imagefontctl.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageDraw, ImageFont
@@ -11,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))
@@ -19,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))
@@ -30,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))
@@ -41,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))
@@ -61,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))
@@ -72,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))
@@ -83,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))
@@ -94,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))
@@ -109,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))
@@ -132,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))
@@ -145,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))
@@ -156,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))
@@ -173,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))
@@ -184,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))
@@ -207,7 +208,9 @@ def test_language():
),
ids=("None", "ltr", "rtl2", "rtl", "ttb"),
)
-def test_getlength(mode, text, direction, expected):
+def test_getlength(
+ mode: str, text: str, direction: str | None, expected: float
+) -> None:
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im)
@@ -229,7 +232,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: str, direction: str, text: str) -> None:
if text == "i\u0305i" and direction == "ttb":
pytest.skip("fails with this font")
@@ -249,7 +252,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: str) -> None:
text = "f"
path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120)
@@ -305,7 +308,9 @@ 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: str, text: str, dir: str | None, anchor: str | None, epsilon: float
+) -> None:
path = f"Tests/images/test_combine_{name}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
@@ -336,7 +341,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: str, align: str) -> None:
# test that multiline text uses getlength, not getsize or getbbox
path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png"
@@ -354,7 +359,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 21b4dee3c..3b1c14b4e 100644
--- a/Tests/test_imagefontpil.py
+++ b/Tests/test_imagefontpil.py
@@ -1,17 +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))
@@ -25,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()
@@ -38,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 b7683ec18..e23adeb70 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import shutil
import subprocess
@@ -19,7 +20,7 @@ class TestImageGrab:
@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)
@@ -28,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()
@@ -38,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"
):
@@ -51,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":
@@ -81,8 +82,9 @@ $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)
+ assert p.stdin is not None
p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"')
p.communicate()
@@ -91,8 +93,9 @@ $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)
+ assert p.stdin is not None
p.stdin.write(
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
$ms = new-object System.IO.MemoryStream(, $bytes)
@@ -112,9 +115,21 @@ $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: str) -> None:
image_path = "Tests/images/hopper." + ext
with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp)
im = ImageGrab.grabclipboard()
assert_image_equal_tofile(im, image_path)
+
+ @pytest.mark.skipif(
+ (
+ sys.platform != "linux"
+ or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy"))
+ ),
+ reason="Linux with wl-clipboard only",
+ )
+ @pytest.mark.parametrize("arg", ("text", "--clear"))
+ def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None:
+ subprocess.call(["wl-copy", arg])
+ assert ImageGrab.grabclipboard() is None
diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py
index 22de86c7c..a21e2307d 100644
--- a/Tests/test_imagemath.py
+++ b/Tests/test_imagemath.py
@@ -1,15 +1,16 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageMath
-def pixel(im):
- if hasattr(im, "im"):
- return f"{im.mode} {repr(im.getpixel((0, 0)))}"
+def pixel(im: Image.Image | int) -> str | int:
if isinstance(im, int):
return int(im) # hack to deal with booleans
+ return f"{im.mode} {repr(im.getpixel((0, 0)))}"
+
A = Image.new("L", (1, 1), 1)
B = Image.new("L", (1, 1), 2)
@@ -23,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"
@@ -32,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"
@@ -59,41 +60,51 @@ def test_ops():
"(lambda: (lambda: exec('pass'))())()",
),
)
-def test_prevent_exec(expression):
+def test_prevent_exec(expression: str) -> 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"
@@ -102,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"
@@ -194,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 ec55aadf9..46b473d7a 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -1,5 +1,8 @@
# Test the ImageMorphology functionality
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImageMorph, _imagingmorph
@@ -7,7 +10,7 @@ from PIL import Image, ImageMorph, _imagingmorph
from .helper import assert_image_equal_tofile, hopper
-def string_to_img(image_string):
+def string_to_img(image_string: str) -> Image.Image:
"""Turn a string image representation into a binary image"""
rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)]
height = len(rows)
@@ -35,7 +38,7 @@ A = string_to_img(
)
-def img_to_string(im):
+def img_to_string(im: Image.Image) -> str:
"""Turn a (small) binary image into a string representation"""
chars = ".1"
width, height = im.size
@@ -45,31 +48,22 @@ def img_to_string(im):
)
-def img_string_normalize(im):
+def img_string_normalize(im: str) -> str:
return img_to_string(string_to_img(im))
-def assert_img_equal_img_string(a, b_string):
+def assert_img_equal_img_string(a: Image.Image, b_string: str) -> 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: str) -> None:
lb = ImageMorph.LutBuilder(op_name=op)
assert lb.get_lut() is None
@@ -78,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)
@@ -92,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)
@@ -111,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)
@@ -130,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)
@@ -149,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)
@@ -168,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)
@@ -196,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)
@@ -215,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)
@@ -234,7 +228,7 @@ def test_negate():
)
-def test_incorrect_mode():
+def test_incorrect_mode() -> None:
im = hopper("RGB")
mop = ImageMorph.MorphOp(op_name="erosion8")
@@ -249,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"]
@@ -267,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"]
@@ -284,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()
@@ -295,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")
@@ -309,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()
@@ -322,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 7980bead0..b320e79c1 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageDraw, ImageOps, ImageStat, features
@@ -13,7 +14,7 @@ from .helper import (
class Deformer:
- def getmesh(self, im):
+ def getmesh(self, im: Image.Image) -> list[tuple[tuple[int, ...], tuple[int, ...]]]:
x, y = im.size
return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))]
@@ -21,7 +22,7 @@ class Deformer:
deformer = Deformer()
-def test_sanity():
+def test_sanity() -> None:
ImageOps.autocontrast(hopper("L"))
ImageOps.autocontrast(hopper("RGB"))
@@ -83,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)
@@ -95,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
@@ -107,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: tuple[int, int]) -> 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
@@ -131,13 +132,13 @@ def test_contain_round():
("hopper.png", (256, 256)), # square
),
)
-def test_cover(image_name, expected_size):
+def test_cover(image_name: str, expected_size: tuple[int, int]) -> 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)
@@ -157,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
@@ -167,7 +168,7 @@ def test_pad_round():
@pytest.mark.parametrize("mode", ("P", "PA"))
-def test_palette(mode):
+def test_palette(mode: str) -> None:
im = hopper(mode)
# Expand
@@ -181,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))
@@ -191,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))
@@ -209,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: int | tuple[int, int, int, int]) -> None:
with Image.open("Tests/images/p_16.tga") as im:
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
@@ -235,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)
@@ -269,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)
@@ -305,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)
@@ -358,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: Image.Image) -> None:
for im in [
orientation_im,
orientation_im.copy(),
@@ -422,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
@@ -434,24 +435,24 @@ def test_exif_transpose_in_place():
assert_image_equal(im, expected)
-def test_autocontrast_unsupported_mode():
+def test_autocontrast_unsupported_mode() -> None:
im = Image.new("RGBA", (1, 1))
with pytest.raises(OSError):
ImageOps.autocontrast(im)
-def test_autocontrast_cutoff():
+def test_autocontrast_cutoff() -> None:
# Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:
- def autocontrast(cutoff):
+ def autocontrast(cutoff: int | tuple[int, int]):
return ImageOps.autocontrast(img, cutoff).histogram()
assert autocontrast(10) == autocontrast((10, 10))
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)
@@ -470,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)
@@ -485,20 +486,20 @@ def test_autocontrast_mask_real_input():
assert result_nomask != result
assert_tuple_approx_equal(
ImageStat.Stat(result, mask=rect_mask).median,
- [195, 202, 184],
+ (195, 202, 184),
threshold=2,
msg="autocontrast with mask pixel incorrect",
)
assert_tuple_approx_equal(
ImageStat.Stat(result_nomask).median,
- [119, 106, 79],
+ (119, 106, 79),
threshold=2,
msg="autocontrast without mask pixel incorrect",
)
-def test_autocontrast_preserve_tone():
- def autocontrast(mode, preserve_tone):
+def test_autocontrast_preserve_tone() -> None:
+ def autocontrast(mode: str, preserve_tone: bool) -> Image.Image:
im = hopper(mode)
return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()
@@ -506,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.
@@ -532,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: tuple[int, int, int]) -> 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 84d3a6950..519d79105 100644
--- a/Tests/test_imageops_usm.py
+++ b/Tests/test_imageops_usm.py
@@ -1,11 +1,14 @@
from __future__ import annotations
+
+from typing import Generator
+
import pytest
from PIL import Image, ImageFilter
@pytest.fixture
-def test_images():
+def test_images() -> Generator[dict[str, Image.Image], None, None]:
ims = {
"im": Image.open("Tests/images/hopper.ppm"),
"snakes": Image.open("Tests/images/color_snakes.png"),
@@ -17,7 +20,7 @@ def test_images():
im.close()
-def test_filter_api(test_images):
+def test_filter_api(test_images: dict[str, Image.Image]) -> None:
im = test_images["im"]
test_filter = ImageFilter.GaussianBlur(2.0)
@@ -31,7 +34,7 @@ def test_filter_api(test_images):
assert i.size == (128, 128)
-def test_usm_formats(test_images):
+def test_usm_formats(test_images: dict[str, Image.Image]) -> None:
im = test_images["im"]
usm = ImageFilter.UnsharpMask
@@ -49,7 +52,7 @@ def test_usm_formats(test_images):
im.convert("YCbCr").filter(usm)
-def test_blur_formats(test_images):
+def test_blur_formats(test_images: dict[str, Image.Image]) -> None:
im = test_images["im"]
blur = ImageFilter.GaussianBlur
@@ -67,7 +70,7 @@ def test_blur_formats(test_images):
im.convert("YCbCr").filter(blur)
-def test_usm_accuracy(test_images):
+def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None:
snakes = test_images["snakes"]
src = snakes.convert("RGB")
@@ -76,7 +79,7 @@ def test_usm_accuracy(test_images):
assert i.tobytes() == src.tobytes()
-def test_blur_accuracy(test_images):
+def test_blur_accuracy(test_images: dict[str, Image.Image]) -> 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 e5b59b74a..8e2db15aa 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImagePalette
@@ -6,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
@@ -45,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
@@ -64,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: int, palette: ImagePalette.ImagePalette) -> None:
im = Image.new("P", (1, 1))
# Do not use transparency index as a new color
@@ -78,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")
@@ -96,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
@@ -112,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
@@ -123,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
@@ -141,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)
@@ -155,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)
@@ -167,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)
@@ -180,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")
@@ -192,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 ac3ea3281..9487560af 100644
--- a/Tests/test_imagepath.py
+++ b/Tests/test_imagepath.py
@@ -1,14 +1,16 @@
from __future__ import annotations
+
import array
import math
import struct
+from typing import Sequence
import pytest
from PIL import Image, ImagePath
-def test_path():
+def test_path() -> None:
p = ImagePath.Path(list(range(10)))
# sequence interface
@@ -56,7 +58,9 @@ def test_path():
ImagePath.Path((0, 1)),
),
)
-def test_path_constructors(coords):
+def test_path_constructors(
+ coords: Sequence[float] | array.array[float] | ImagePath.Path,
+) -> None:
# Arrange / Act
p = ImagePath.Path(coords)
@@ -74,7 +78,9 @@ def test_path_constructors(coords):
[[0.0, 1.0]],
),
)
-def test_invalid_path_constructors(coords):
+def test_invalid_path_constructors(
+ coords: tuple[str, str] | Sequence[Sequence[int]]
+) -> None:
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
@@ -92,7 +98,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: Sequence[int]) -> None:
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
@@ -110,7 +116,9 @@ 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: int | list[int], expected: tuple[float, float, float, float]
+) -> None:
# Arrange
p = ImagePath.Path(coords)
@@ -118,7 +126,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])
@@ -134,7 +142,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: int | list[int], expected: list[tuple[float, float]]) -> None:
# Arrange
p = ImagePath.Path(coords)
@@ -146,7 +154,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
@@ -164,7 +172,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
@@ -183,7 +191,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)):
@@ -197,12 +205,12 @@ def test_overflow_segfault():
class Evil:
- def __init__(self):
+ def __init__(self) -> None:
self.corrupt = Image.core.path(0x4000000000000000)
- def __getitem__(self, i):
+ def __getitem__(self, i: int) -> bytes:
x = self.corrupt[i]
return struct.pack("dd", x[0], x[1])
- def __setitem__(self, i, x):
+ def __setitem__(self, i: int, x: bytes) -> None:
self.corrupt[i] = struct.unpack("dd", x)
diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py
index 41d247f42..88ad1f9ee 100644
--- a/Tests/test_imageqt.py
+++ b/Tests/test_imageqt.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
@@ -15,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,
@@ -27,7 +28,7 @@ def test_rgb():
assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255)
- def checkrgb(r, g, b):
+ def checkrgb(r: int, g: int, b: int) -> None:
val = ImageQt.rgb(r, g, b)
val = val % 2**24 # drop the alpha
assert val >> 16 == r
@@ -40,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+
@@ -54,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 6d71e4d87..7280dded0 100644
--- a/Tests/test_imagesequence.py
+++ b/Tests/test_imagesequence.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image, ImageSequence, TiffImagePlugin
@@ -6,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")
@@ -26,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):
@@ -37,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()
@@ -52,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):
@@ -74,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]
@@ -83,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 761d28d30..8d741d94a 100644
--- a/Tests/test_imageshow.py
+++ b/Tests/test_imageshow.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from typing import Any
+
import pytest
from PIL import Image, ImageShow
@@ -6,12 +9,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")
@@ -23,9 +26,9 @@ def test_register():
"order",
[-1, 0],
)
-def test_viewer_show(order):
+def test_viewer_show(order: int) -> None:
class TestViewer(ImageShow.Viewer):
- def show_image(self, image, **options):
+ def show_image(self, image: Image.Image, **options: Any) -> bool:
self.methodCalled = True
return True
@@ -47,12 +50,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: str) -> None:
im = hopper(mode)
assert ImageShow.show(im)
-def test_show_without_viewers():
+def test_show_without_viewers() -> None:
viewers = ImageShow._viewers
ImageShow._viewers = []
@@ -62,7 +65,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
@@ -72,14 +75,14 @@ def test_viewer():
@pytest.mark.parametrize("viewer", ImageShow._viewers)
-def test_viewers(viewer):
+def test_viewers(viewer: ImageShow.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):
diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py
index 7b56b89cc..b1c1306c1 100644
--- a/Tests/test_imagestat.py
+++ b/Tests/test_imagestat.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageStat
@@ -6,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)
@@ -30,7 +31,7 @@ def test_sanity():
ImageStat.Stat(1)
-def test_hopper():
+def test_hopper() -> None:
im = hopper()
st = ImageStat.Stat(im)
@@ -43,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 bb20fbb6f..b607b8c43 100644
--- a/Tests/test_imagetk.py
+++ b/Tests/test_imagetk.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -22,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()
@@ -33,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:
@@ -56,7 +57,7 @@ def test_kw():
@pytest.mark.parametrize("mode", TK_MODES)
-def test_photoimage(mode):
+def test_photoimage(mode: str) -> None:
# test as image:
im = hopper(mode)
@@ -70,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)
@@ -78,7 +79,7 @@ def test_photoimage_apply_transparency():
@pytest.mark.parametrize("mode", TK_MODES)
-def test_photoimage_blank(mode):
+def test_photoimage_blank(mode: str) -> None:
# test a image using mode/size:
im_tk = ImageTk.PhotoImage(mode, (100, 100))
@@ -90,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 6927eedcf..b43c31b52 100644
--- a/Tests/test_imagewin.py
+++ b/Tests/test_imagewin.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import ImageWin
@@ -7,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
@@ -21,7 +22,7 @@ class TestImageWin:
# Assert
assert dc2 == 50
- def test_hwnd(self):
+ def test_hwnd(self) -> None:
# Arrange
wnd = 50
@@ -35,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()
@@ -45,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)
@@ -56,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()
@@ -70,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)
@@ -85,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 bd154335a..f59ee7284 100644
--- a/Tests/test_imagewin_pointers.py
+++ b/Tests/test_imagewin_pointers.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
from io import BytesIO
+from pathlib import Path
from PIL import Image, ImageWin
@@ -68,7 +70,7 @@ if is_win32():
]
CreateDIBSection.restype = ctypes.wintypes.HBITMAP
- def serialize_dib(bi, pixels):
+ def serialize_dib(bi, pixels) -> bytearray:
bf = BITMAPFILEHEADER()
bf.bfType = 0x4D42
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize
@@ -82,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 92cad4ac1..31548bbc9 100644
--- a/Tests/test_lib_image.py
+++ b/Tests/test_lib_image.py
@@ -1,10 +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 1293f7628..629a6dc7a 100644
--- a/Tests/test_lib_pack.py
+++ b/Tests/test_lib_pack.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
import pytest
@@ -9,7 +10,13 @@ X = 255
class TestLibPack:
- def assert_pack(self, mode, rawmode, data, *pixels):
+ def assert_pack(
+ self,
+ mode: str,
+ rawmode: str,
+ data: int | bytes,
+ *pixels: int | float | tuple[int, ...],
+ ) -> None:
"""
data - either raw bytes with data or just number of bytes in rawmode.
"""
@@ -23,7 +30,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)
@@ -36,29 +43,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)
@@ -78,7 +85,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)
@@ -100,12 +107,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)
@@ -133,7 +140,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",
@@ -148,7 +155,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(
@@ -171,19 +178,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
@@ -208,10 +215,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":
@@ -227,7 +234,13 @@ class TestLibPack:
class TestLibUnpack:
- def assert_unpack(self, mode, rawmode, data, *pixels):
+ def assert_unpack(
+ self,
+ mode: str,
+ rawmode: str,
+ data: int | bytes,
+ *pixels: int | float | tuple[int, ...],
+ ) -> None:
"""
data - either raw bytes with data or just number of bytes in rawmode.
"""
@@ -240,7 +253,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)
@@ -253,7 +266,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)
@@ -272,14 +285,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?
@@ -290,11 +303,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))
@@ -345,14 +358,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)
@@ -521,7 +534,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)
)
@@ -535,7 +548,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))
@@ -580,7 +593,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)
)
@@ -618,25 +631,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)
@@ -677,7 +690,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)
@@ -716,7 +729,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
)
@@ -767,7 +780,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)
@@ -784,7 +797,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":
@@ -792,7 +805,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 49b052fa4..1c8b84a2b 100644
--- a/Tests/test_locale.py
+++ b/Tests/test_locale.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import locale
import pytest
@@ -23,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 a84e61a7b..46259f1dc 100644
--- a/Tests/test_main.py
+++ b/Tests/test_main.py
@@ -1,10 +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 76444f33d..93140f6e5 100644
--- a/Tests/test_map.py
+++ b/Tests/test_map.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
import pytest
@@ -6,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
@@ -24,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
@@ -38,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 3e17d8dcc..903f7e0c6 100644
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import Image
@@ -8,7 +11,7 @@ from .helper import hopper
original = hopper().resize((32, 32)).convert("I")
-def verify(im1):
+def verify(im1: Image.Image) -> None:
im2 = original.copy()
assert im1.size == im2.size
pix1 = im1.load()
@@ -24,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: str) -> None:
# PIL 1.1 has limited support for 16-bit image data. Check that
# create/copy/transform and save works as expected.
@@ -74,8 +77,8 @@ def test_basic(tmp_path, mode):
assert im_in.getpixel((0, 0)) == min(512, maximum)
-def test_tobytes():
- def tobytes(mode):
+def test_tobytes() -> None:
+ def tobytes(mode: str) -> Image.Image:
return Image.new(mode, (1, 1), 1).tobytes()
order = 1 if Image._ENDIAN == "<" else -1
@@ -86,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 6f0e99b3f..9f4e6534e 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
@@ -12,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) -> Image.Image:
if bands == 1:
if boolean:
data = [0, 255] * 50
@@ -81,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)
@@ -93,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: Image.Image, np) -> None:
assert len(np.shape) >= 2
np_size = np.shape[1], np.shape[0]
assert img.size == np_size
@@ -108,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")
@@ -125,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(
@@ -156,7 +157,7 @@ def test_save_tiff_uint16():
("HSV", numpy.uint8),
),
)
-def test_to_array(mode, dtype):
+def test_to_array(mode: str, dtype) -> None:
img = hopper(mode)
# Resize to non-square
@@ -168,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
@@ -179,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
@@ -206,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
@@ -221,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
@@ -236,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 aeeafb6f1..f6b12cb20 100644
--- a/Tests/test_pdfparser.py
+++ b/Tests/test_pdfparser.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import time
import pytest
@@ -18,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)
@@ -36,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)
@@ -94,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"
@@ -120,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 eb687b57b..ed415953f 100644
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+
import pickle
+from pathlib import Path
import pytest
@@ -11,7 +13,9 @@ 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, protocol: int, test_file: str, mode: str | None
+) -> None:
# Arrange
with Image.open(test_file) as im:
filename = str(tmp_path / "temp.pkl")
@@ -28,7 +32,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(protocol: int, test_file: str, mode: str | None) -> None:
with Image.open(test_file) as im:
if mode:
im = im.convert(mode)
@@ -62,13 +66,15 @@ 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: str, test_mode: str | None, protocol: int
+) -> None:
# Act / Assert
- helper_pickle_string(pickle, protocol, test_file, test_mode)
- helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode)
+ helper_pickle_string(protocol, test_file, test_mode)
+ helper_pickle_file(tmp_path, 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:
@@ -87,7 +93,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
@@ -97,7 +103,9 @@ def test_pickle_tell():
assert unpickled_image.tell() == 0
-def helper_assert_pickled_font_images(font1, font2):
+def helper_assert_pickled_font_images(
+ font1: ImageFont.FreeTypeFont, font2: ImageFont.FreeTypeFont
+) -> None:
# Arrange
im1 = Image.new(mode="RGBA", size=(300, 100))
im2 = Image.new(mode="RGBA", size=(300, 100))
@@ -115,7 +123,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: int) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
@@ -129,7 +137,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: int) -> 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 77c7952e9..64dfb2c95 100644
--- a/Tests/test_psdraw.py
+++ b/Tests/test_psdraw.py
@@ -1,14 +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: PSDraw.PSDraw) -> None:
title = "hopper"
box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points
@@ -30,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
@@ -48,7 +50,7 @@ def test_draw_postscript(tmp_path):
@pytest.mark.parametrize("buffer", (True, False))
-def test_stdout(buffer):
+def test_stdout(buffer: bool) -> None:
# Temporarily redirect stdout
old_stdout = sys.stdout
diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py
index 08133b6c3..c2f7fe22e 100644
--- a/Tests/test_pyroma.py
+++ b/Tests/test_pyroma.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import __version__
@@ -6,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 49ca01677..3cd323553 100644
--- a/Tests/test_qt_image_qapplication.py
+++ b/Tests/test_qt_image_qapplication.py
@@ -1,7 +1,10 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
-from PIL import ImageQt
+from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@@ -18,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))
@@ -34,14 +37,14 @@ if ImageQt.qt_is_installed:
lbl.setPixmap(pixmap1.copy())
-def roundtrip(expected):
+def roundtrip(expected: Image.Image) -> 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 396bd9080..6110be707 100644
--- a/Tests/test_qt_image_toqimage.py
+++ b/Tests/test_qt_image_toqimage.py
@@ -1,4 +1,7 @@
from __future__ import annotations
+
+from pathlib import Path
+
import pytest
from PIL import ImageQt
@@ -14,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: str, 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 37d72d451..3ce31cd2d 100644
--- a/Tests/test_sgi_crash.py
+++ b/Tests/test_sgi_crash.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
@@ -20,7 +21,7 @@ from PIL import Image
"Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi",
],
)
-def test_crashes(test_file):
+def test_crashes(test_file: str) -> 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 d93b03904..2a072fd44 100644
--- a/Tests/test_shell_injection.py
+++ b/Tests/test_shell_injection.py
@@ -1,5 +1,8 @@
from __future__ import annotations
+
import shutil
+from pathlib import Path
+from typing import Callable
import pytest
@@ -15,7 +18,12 @@ 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: Image.Image,
+ save_func: Callable[[Image.Image, int, str], None],
+ ) -> None:
for filename in test_filenames:
dest_file = str(tmp_path / filename)
save_func(src_img, 0, dest_file)
@@ -24,7 +32,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)
@@ -33,18 +41,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 64e781cba..f51e8b3a8 100644
--- a/Tests/test_tiff_crashes.py
+++ b/Tests/test_tiff_crashes.py
@@ -42,7 +42,7 @@ from .helper import on_ci
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
@pytest.mark.filterwarnings("ignore:Metadata warning")
@pytest.mark.filterwarnings("ignore:Truncated File Read")
-def test_tiff_crashes(test_file):
+def test_tiff_crashes(test_file: str) -> None:
try:
with Image.open(test_file) as im:
im.load()
diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py
index e7b41fb47..f6adae3e6 100644
--- a/Tests/test_tiff_ifdrational.py
+++ b/Tests/test_tiff_ifdrational.py
@@ -1,5 +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
@@ -7,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))
@@ -30,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)
@@ -50,10 +52,10 @@ def test_nonetype():
assert xres and yres
-def test_ifd_rational_save(tmp_path):
- methods = (True, False)
- if not features.check("libtiff"):
- methods = (False,)
+def test_ifd_rational_save(tmp_path: Path) -> None:
+ methods = [True]
+ if features.check("libtiff"):
+ methods.append(False)
for libtiff in methods:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py
index 6b693f7cd..d55ceb4be 100644
--- a/Tests/test_uploader.py
+++ b/Tests/test_uploader.py
@@ -1,14 +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 1457d85f7..197ef79ee 100644
--- a/Tests/test_util.py
+++ b/Tests/test_util.py
@@ -1,26 +1,16 @@
from __future__ import annotations
+
+from pathlib import Path, PurePath
+
import pytest
from PIL import _util
-def test_is_path():
- # Arrange
- fp = "filename.ext"
-
- # Act
- it_is = _util.is_path(fp)
-
- # Assert
- assert it_is
-
-
-def test_path_obj_is_path():
- # Arrange
- from pathlib import Path
-
- test_path = Path("filename.ext")
-
+@pytest.mark.parametrize(
+ "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")]
+)
+def test_is_path(test_path: str | Path | PurePath) -> None:
# Act
it_is = _util.is_path(test_path)
@@ -28,7 +18,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
@@ -40,7 +30,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"
@@ -51,7 +41,7 @@ def test_is_directory():
assert it_is
-def test_is_not_directory():
+def test_is_not_directory() -> None:
# Arrange
text = "abc"
@@ -62,11 +52,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 28ebc7d79..626fe427c 100644
--- a/Tests/test_webp_leaks.py
+++ b/Tests/test_webp_leaks.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
from PIL import Image
@@ -13,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/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/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 9974b0f2a..97289c91d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -54,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"
@@ -233,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').
@@ -326,7 +326,7 @@ linkcheck_allowed_redirects = {
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/.*",
+ 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]+",
}
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 75c0b73eb..205fcb9ab 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -44,6 +44,54 @@ ImageFile.raise_oserror
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
----------------
@@ -107,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::
@@ -327,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 :py:exc:`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
diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py
index e98bb8680..2a2a0ba29 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
diff --git a/docs/example/anchors.py b/docs/example/anchors.py
index 3a0e40b84..b5d76b4fe 100644
--- a/docs/example/anchors.py
+++ b/docs/example/anchors.py
@@ -5,7 +5,7 @@ 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 9cd65fd48..569ccb769 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -487,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.
@@ -552,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
@@ -685,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
^^^
diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst
index d79f2465f..523e2ad74 100644
--- a/docs/handbook/tutorial.rst
+++ b/docs/handbook/tutorial.rst
@@ -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))
diff --git a/docs/index.rst b/docs/index.rst
index 4f577fe9c..bf2feea9a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_ 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/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/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_design.rst b/docs/reference/internal_design.rst
index 2e2d3322f..99a18e9ea 100644
--- a/docs/reference/internal_design.rst
+++ b/docs/reference/internal_design.rst
@@ -1,5 +1,5 @@
-Internal Reference Docs
-=======================
+Internal Reference
+==================
.. toctree::
:maxdepth: 2
diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst
index 363a67d9b..899e4966f 100644
--- a/docs/reference/internal_modules.rst
+++ b/docs/reference/internal_modules.rst
@@ -25,6 +25,27 @@ 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:class:: StrOrBytesPath
+
+ Typing alias.
+
+.. py:class:: SupportsRead
+
+ An object that supports the read method.
+
+.. py:data:: TypeGuard
+ :value: typing.TypeGuard
+
+ See :py:obj:`typing.TypeGuard`.
+
:mod:`~PIL._util` Module
------------------------
diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst
index f31941c9a..730c8da5b 100644
--- a/docs/reference/open_files.rst
+++ b/docs/reference/open_files.rst
@@ -3,7 +3,7 @@
File Handling in Pillow
=======================
-When opening a file as an image, Pillow requires a filename, ``pathlib.Path``
+When opening a file as an image, Pillow requires a filename, ``os.PathLike``
object, or a file-like object. Pillow uses the filename or ``Path`` to open a
file, so for the rest of this article, they will all be treated as a file-like
object.
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index a3f238119..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
diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst
index 9883f10ba..c3947f64c 100644
--- a/docs/releasenotes/10.2.0.rst
+++ b/docs/releasenotes/10.2.0.rst
@@ -1,14 +1,6 @@
10.2.0
------
-Backwards Incompatible Changes
-==============================
-
-TODO
-^^^^
-
-TODO
-
Deprecations
============
@@ -20,10 +12,14 @@ ImageFile.raise_oserror
error codes returned by a codec's ``decode()`` method, which ImageFile already does
automatically.
-TODO
-^^^^
+IptcImageFile helper functions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
+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
===========
@@ -46,6 +42,14 @@ Added DdsImagePlugin enums
: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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -62,10 +66,34 @@ output only the quantization and Huffman tables for the image.
Security
========
-TODO
-^^^^
+ImageFont.getmask: Applied ImageFont.MAX_STRING_LENGTH
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
+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
=============
@@ -78,16 +106,56 @@ 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 3x as fast
-on average and ``st.extrema`` is 12x as fast on average.
+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..af31cdb74
--- /dev/null
+++ b/docs/releasenotes/10.3.0.rst
@@ -0,0 +1,87 @@
+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.
+
+Release GIL when fetching WebP frames
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Python's Global Interpreter Lock is now released when fetching WebP frames from
+the libwebp decoder.
diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst
index 2bf299dd3..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
diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst
index 02da702a7..6400218f4 100644
--- a/docs/releasenotes/9.1.0.rst
+++ b/docs/releasenotes/9.1.0.rst
@@ -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 d8034853c..e86f8082b 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 10.3.0
10.2.0
10.1.0
10.0.1
diff --git a/pyproject.toml b/pyproject.toml
index 193e8c9b2..58c2464bc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,7 @@ classifiers = [
"Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
"Topic :: Multimedia :: Graphics :: Viewers",
+ "Typing :: Typed",
]
dynamic = [
"version",
@@ -65,6 +66,9 @@ tests = [
"pytest-cov",
"pytest-timeout",
]
+typing = [
+ 'typing-extensions; python_version < "3.10"',
+]
xmp = [
"defusedxml",
]
@@ -93,7 +97,7 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests"
-[tool.ruff]
+[tool.ruff.lint]
select = [
"C4", # flake8-comprehensions
"E", # pycodestyle errors
@@ -101,24 +105,25 @@ select = [
"F", # pyflakes errors
"I", # isort
"ISC", # flake8-implicit-str-concat
+ "LOG", # flake8-logging
"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 = [
+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/*.py" = ["I001"]
+[tool.ruff.lint.per-file-ignores]
+"Tests/oss-fuzz/fuzz_font.py" = ["I002"]
+"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"]
-[tool.ruff.isort]
+[tool.ruff.lint.isort]
known-first-party = ["PIL"]
required-imports = ["from __future__ import annotations"]
@@ -136,23 +141,3 @@ 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/ImageCms.py$',
- '^src/PIL/ImageFile.py$',
- '^src/PIL/ImageFont.py$',
- '^src/PIL/ImageMath.py$',
- '^src/PIL/ImageMorph.py$',
- '^src/PIL/ImageQt.py$',
- '^src/PIL/ImageShow.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 600fd6496..ed5252c44 100755
--- a/selftest.py
+++ b/selftest.py
@@ -15,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.py b/setup.py
old mode 100755
new mode 100644
index 07163d001..0ad9204d0
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python3
# > pyroma .
# ------------------------------
# Checking .
diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py
index b12ddc2d4..e3eda4fe9 100644
--- a/src/PIL/BdfFontFile.py
+++ b/src/PIL/BdfFontFile.py
@@ -22,6 +22,8 @@ Parse X Bitmap Distribution Format (BDF)
"""
from __future__ import annotations
+from typing import BinaryIO
+
from . import FontFile, Image
bdf_slant = {
@@ -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 b8f38b78a..f0fbc8cc2 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
diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py
index 387a4c182..0035296a4 100644
--- a/src/PIL/ContainerIO.py
+++ b/src/PIL/ContainerIO.py
@@ -16,15 +16,16 @@
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/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index 5b6ac2ead..be17f4223 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/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 io
@@ -18,6 +19,7 @@ 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 ")
@@ -268,13 +270,17 @@ class D3DFMT(IntEnum):
# Backward compatibility layer
module = sys.modules[__name__]
for item in DDSD:
+ assert item.name is not None
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)
+for item1 in DDSCAPS:
+ assert item1.name is not None
+ setattr(module, "DDSCAPS_" + item1.name, item1.value)
+for item2 in DDSCAPS2:
+ assert item2.name is not None
+ setattr(module, "DDSCAPS2_" + item2.name, item2.value)
+for item3 in DDPF:
+ assert item3.name is not None
+ setattr(module, "DDPF_" + item3.name, item3.value)
DDS_FOURCC = DDPF.FOURCC
DDS_RGB = DDPF.RGB
@@ -341,6 +347,7 @@ class DdsImageFile(ImageFile.ImageFile):
flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height)
+ extents = (0, 0) + self.size
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved
@@ -351,22 +358,16 @@ class DdsImageFile(ImageFile.ImageFile):
rawmode = None
if pfflags & DDPF.RGB:
# Texture contains uncompressed RGB data
- masks = struct.unpack("<4I", header.read(16))
- masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
- if bitcount == 24:
- self._mode = "RGB"
- rawmode = masks[0x000000FF] + masks[0x0000FF00] + masks[0x00FF0000]
- elif bitcount == 32 and pfflags & DDPF.ALPHAPIXELS:
+ if pfflags & DDPF.ALPHAPIXELS:
self._mode = "RGBA"
- rawmode = (
- masks[0x000000FF]
- + masks[0x0000FF00]
- + masks[0x00FF0000]
- + masks[0xFF000000]
- )
+ mask_count = 4
else:
- msg = f"Unsupported bitcount {bitcount} for {pfflags}"
- raise OSError(msg)
+ self._mode = "RGB"
+ mask_count = 3
+
+ masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
+ self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
+ return
elif pfflags & DDPF.LUMINANCE:
if bitcount == 8:
self._mode = "L"
@@ -464,7 +465,6 @@ class DdsImageFile(ImageFile.ImageFile):
msg = f"Unknown pixel format flags {pfflags}"
raise NotImplementedError(msg)
- extents = (0, 0) + self.size
if n:
self.tile = [
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
@@ -476,6 +476,39 @@ class DdsImageFile(ImageFile.ImageFile):
pass
+class DdsRgbDecoder(ImageFile.PyDecoder):
+ _pulls_fd = True
+
+ def decode(self, buffer):
+ bitcount, masks = self.args
+
+ # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
+ # Calculate how many zeros each mask is padded with
+ mask_offsets = []
+ # And the maximum value of each channel without the padding
+ mask_totals = []
+ for mask in masks:
+ offset = 0
+ if mask != 0:
+ while mask >> (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"
@@ -533,5 +566,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/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index 7dce2d60f..e69890bab 100644
--- a/src/PIL/FitsImagePlugin.py
+++ b/src/PIL/FitsImagePlugin.py
@@ -15,7 +15,7 @@ import math
from . import Image, ImageFile
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:6] == b"SIMPLE"
@@ -23,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:
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index 9769761fc..f9e4c731c 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -77,6 +77,7 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF100:
# prefix chunk; ignore it
self.__offset = self.__offset + i32(s)
+ self.fp.seek(self.__offset)
s = self.fp.read(16)
if i16(s, 4) == 0xF1FA:
diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py
index 9621770e2..1e0c1c166 100644
--- a/src/PIL/FontFile.py
+++ b/src/PIL/FontFile.py
@@ -16,13 +16,16 @@
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,32 @@ 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 +70,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,13 +84,16 @@ 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]
@@ -88,12 +110,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
@@ -104,6 +129,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/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py
index d5513a56a..b4488e6ee 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
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
index d84876eb6..88b87a22c 100644
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -27,9 +27,12 @@
"""
from __future__ import annotations
+from typing import IO
+
from . import ImageFile, ImagePalette, UnidentifiedImageError
from ._binary import i16be as i16
from ._binary import i32be as i32
+from ._typing import StrOrBytesPath
class GdImageFile(ImageFile.ImageFile):
@@ -43,8 +46,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 +81,7 @@ class GdImageFile(ImageFile.ImageFile):
]
-def open(fp, mode="r"):
+def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile:
"""
Load texture from a GD image file.
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 57d87078b..dc842d7a3 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -641,9 +641,9 @@ def _write_multiple_frames(im, fp, palette):
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)
+ encoderinfo["transparency"] = (
+ im_frame.palette._new_color_index(im_frame)
+ )
except ValueError:
pass
if "transparency" in encoderinfo:
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 97d726a8a..4613e40b6 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -93,8 +93,8 @@ for i in ["16", "16L", "16B"]:
for i in ["32S"]:
OPEN[f"L {i} image"] = ("I", f"I;{i}")
OPEN[f"L*{i} image"] = ("I", f"I;{i}")
-for i in range(2, 33):
- OPEN[f"L*{i} image"] = ("F", f"F;{i}")
+for j in range(2, 33):
+ OPEN[f"L*{j} image"] = ("F", f"F;{j}")
# --------------------------------------------------------------------
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 1cb484b85..ba81a22c7 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -26,6 +26,7 @@
from __future__ import annotations
+import abc
import atexit
import builtins
import io
@@ -39,12 +40,8 @@ import tempfile
import warnings
from collections.abc import Callable, MutableMapping
from enum import IntEnum
-from pathlib import Path
-
-try:
- from defusedxml import ElementTree
-except ImportError:
- ElementTree = None
+from types import ModuleType
+from typing import IO, TYPE_CHECKING, Any
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
@@ -60,6 +57,12 @@ from . import (
from ._binary import i32le, o32be, o32le
from ._util import DeferredError, is_path
+ElementTree: ModuleType | None
+try:
+ from defusedxml import ElementTree
+except ImportError:
+ ElementTree = None
+
logger = logging.getLogger(__name__)
@@ -92,7 +95,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
@@ -110,6 +113,7 @@ except ImportError as v:
USE_CFFI_ACCESS = False
+cffi: ModuleType | None
try:
import cffi
except ImportError:
@@ -211,14 +215,22 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# --------------------------------------------------------------------
# Registries
-ID = []
-OPEN = {}
-MIME = {}
-SAVE = {}
-SAVE_ALL = {}
-EXTENSION = {}
-DECODERS = {}
-ENCODERS = {}
+if TYPE_CHECKING:
+ from . import ImageFile
+ID: list[str] = []
+OPEN: dict[
+ str,
+ tuple[
+ Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
+ Callable[[bytes], bool] | None,
+ ],
+] = {}
+MIME: dict[str, str] = {}
+SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {}
+SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {}
+EXTENSION: dict[str, str] = {}
+DECODERS: dict[str, object] = {}
+ENCODERS: dict[str, object] = {}
# --------------------------------------------------------------------
# Modes
@@ -242,7 +254,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
@@ -282,7 +294,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.
@@ -530,15 +542,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):
"""
@@ -554,12 +570,7 @@ class Image:
"""
if hasattr(self, "fp"):
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._close_fp()
self.fp = None
except Exception as msg:
logger.debug("Error closing: %s", msg)
@@ -572,7 +583,7 @@ class Image:
# object is gone.
self.im = DeferredError(ValueError("Operation on closed image"))
- def _copy(self):
+ def _copy(self) -> None:
self.load()
self.im = self.im.copy()
self.pyaccess = None
@@ -584,7 +595,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
@@ -709,7 +722,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.
@@ -787,7 +800,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.
@@ -874,7 +887,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
@@ -1181,7 +1194,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.
@@ -1194,7 +1207,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
@@ -1296,7 +1309,7 @@ class Image:
]
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").
@@ -1306,7 +1319,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.
@@ -1417,7 +1430,7 @@ class Image:
root = ElementTree.fromstring(xmp_tags)
return {get_name(root.tag): get_value(root)}
- def getexif(self):
+ def getexif(self) -> Exif:
"""
Gets EXIF data from the image.
@@ -1425,7 +1438,6 @@ class Image:
"""
if self._exif is None:
self._exif = Exif()
- self._exif._loaded = False
elif self._exif._loaded:
return self._exif
self._exif._loaded = True
@@ -1512,7 +1524,7 @@ class Image:
self.load()
return self.im.ptr
- def getpalette(self, rawmode="RGB"):
+ def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None:
"""
Returns the image palette as a list.
@@ -1602,7 +1614,7 @@ class Image:
x, y = self.im.getprojection()
return list(x), list(y)
- def histogram(self, mask=None, extrema=None):
+ def histogram(self, mask=None, extrema=None) -> list[int]:
"""
Returns a histogram for the image. The histogram is returned as a
list of pixel counts, one for each pixel value in the source
@@ -1659,7 +1671,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
@@ -1791,7 +1803,7 @@ class Image:
result = alpha_composite(background, overlay)
self.paste(result, box)
- def point(self, lut, mode=None):
+ def point(self, lut, mode: str | None = None) -> Image:
"""
Maps this image through a lookup table or function.
@@ -1915,7 +1927,7 @@ class Image:
self.im.putdata(data, scale, offset)
- def putpalette(self, data, rawmode="RGB"):
+ def putpalette(self, data, rawmode="RGB") -> None:
"""
Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image.
@@ -2095,7 +2107,7 @@ class Image:
min(self.size[1], math.ceil(box[3] + support_y)),
)
- def resize(self, size, resample=None, box=None, reducing_gap=None):
+ def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image:
"""
Returns a resized copy of this image.
@@ -2187,10 +2199,11 @@ class Image:
if factor_x > 1 or factor_y > 1:
reduce_box = self._get_safe_box(size, resample, box)
factor = (factor_x, factor_y)
- if callable(self.reduce):
- self = self.reduce(factor, box=reduce_box)
- else:
- self = Image.reduce(self, factor, box=reduce_box)
+ self = (
+ self.reduce(factor, box=reduce_box)
+ if callable(self.reduce)
+ else Image.reduce(self, factor, box=reduce_box)
+ )
box = (
(box[0] - reduce_box[0]) / factor_x,
(box[1] - reduce_box[1]) / factor_y,
@@ -2352,7 +2365,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
@@ -2369,7 +2382,7 @@ class Image:
implement the ``seek``, ``tell``, and ``write``
methods, and be opened in binary mode.
- :param fp: A filename (string), pathlib.Path object or file object.
+ :param fp: A filename (string), os.PathLike object or file object.
:param format: Optional format override. If omitted, the
format to use is determined from the filename extension.
If a file object was used instead of a filename, this
@@ -2382,13 +2395,10 @@ class Image:
may have been created, and may contain partial data.
"""
- filename = ""
+ filename: str | bytes = ""
open_fp = False
- if isinstance(fp, Path):
- filename = str(fp)
- open_fp = True
- elif is_path(fp):
- filename = fp
+ if is_path(fp):
+ filename = os.path.realpath(os.fspath(fp))
open_fp = True
elif fp == sys.stdout:
try:
@@ -2397,7 +2407,7 @@ class Image:
pass
if not filename and hasattr(fp, "name") and is_path(fp.name):
# only set the name for metadata purposes
- filename = fp.name
+ filename = os.path.realpath(os.fspath(fp.name))
# may mutate self!
self._ensure_mutable()
@@ -2408,7 +2418,8 @@ class Image:
preinit()
- ext = os.path.splitext(filename)[1].lower()
+ filename_ext = os.path.splitext(filename)[1].lower()
+ ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
if not format:
if ext not in EXTENSION:
@@ -2450,7 +2461,7 @@ class Image:
if open_fp:
fp.close()
- def seek(self, frame):
+ def seek(self, frame) -> None:
"""
Seeks to the given frame in this sequence file. If you seek
beyond the end of the sequence, the method raises an
@@ -2494,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,
@@ -2510,10 +2521,8 @@ class Image:
self.load()
if self.im.bands == 1:
- ims = [self.copy()]
- else:
- ims = map(self._new, self.im.split())
- return tuple(ims)
+ return (self.copy(),)
+ return tuple(map(self._new, self.im.split()))
def getchannel(self, channel):
"""
@@ -2537,7 +2546,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`.
@@ -2644,7 +2653,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
@@ -2667,6 +2676,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::
@@ -2805,7 +2818,7 @@ class Image:
self.im.transform2(box, image.im, method, data, resample, fill)
- def transpose(self, method):
+ def transpose(self, method: Transpose) -> Image:
"""
Transpose image (flip or rotate in 90 degree steps)
@@ -2857,7 +2870,9 @@ class ImagePointHandler:
(for use with :py:meth:`~PIL.Image.Image.point`)
"""
- pass
+ @abc.abstractmethod
+ def point(self, im: Image) -> Image:
+ pass
class ImageTransformHandler:
@@ -2866,7 +2881,14 @@ class ImageTransformHandler:
(for use with :py:meth:`~PIL.Image.Image.transform`)
"""
- pass
+ @abc.abstractmethod
+ def transform(
+ self,
+ size: tuple[int, int],
+ image: Image,
+ **options: dict[str, str | int | tuple[int, ...] | list[int]],
+ ) -> Image:
+ pass
# --------------------------------------------------------------------
@@ -2903,7 +2925,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.
@@ -2942,7 +2964,7 @@ def new(mode, size, color=0):
return im._new(core.fill(mode, size, color))
-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.
@@ -3191,7 +3213,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.
@@ -3201,7 +3223,7 @@ def open(fp, mode="r", formats=None):
:py:meth:`~PIL.Image.Image.load` method). See
:py:func:`~PIL.Image.new`. See :ref:`file-handling`.
- :param fp: A filename (string), pathlib.Path object or a file object.
+ :param fp: A filename (string), os.PathLike object or a file object.
The file object must implement ``file.read``,
``file.seek``, and ``file.tell`` methods,
and be opened in binary mode. The file object will also seek to zero
@@ -3238,11 +3260,9 @@ def open(fp, mode="r", formats=None):
raise TypeError(msg)
exclusive_fp = False
- filename = ""
- if isinstance(fp, Path):
- filename = str(fp.resolve())
- elif is_path(fp):
- filename = fp
+ filename: str | bytes = ""
+ if is_path(fp):
+ filename = os.path.realpath(os.fspath(fp))
if filename:
fp = builtins.open(filename, "rb")
@@ -3416,7 +3436,11 @@ def merge(mode, bands):
# Plugin registry
-def register_open(id, factory, accept=None):
+def register_open(
+ id,
+ factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
+ accept: Callable[[bytes], bool] | None = None,
+) -> None:
"""
Register an image file plugin. This function should not be used
in application code.
@@ -3432,7 +3456,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.
@@ -3447,7 +3471,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.
@@ -3470,7 +3494,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.
@@ -3481,7 +3505,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.
@@ -3502,7 +3526,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.
@@ -3626,7 +3650,13 @@ _apply_env_variables()
atexit.register(core.clear_cache)
-class Exif(MutableMapping):
+if TYPE_CHECKING:
+ _ExifBase = MutableMapping[int, Any]
+else:
+ _ExifBase = MutableMapping
+
+
+class Exif(_ExifBase):
"""
This class provides read and write access to EXIF image data::
@@ -3662,6 +3692,7 @@ class Exif(MutableMapping):
endian = None
bigtiff = False
+ _loaded = False
def __init__(self):
self._data = {}
@@ -3777,7 +3808,7 @@ class Exif(MutableMapping):
return merged_dict
- def tobytes(self, offset=8):
+ def tobytes(self, offset: int = 8) -> bytes:
from . import TiffImagePlugin
head = self._get_head()
@@ -3932,7 +3963,7 @@ class Exif(MutableMapping):
del self._info[tag]
self._data[tag] = value
- def __delitem__(self, tag):
+ def __delitem__(self, tag: int) -> None:
if self._info is not None and tag in self._info:
del self._info[tag]
else:
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 9d27f2513..2b0ed6c9d 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.
@@ -16,10 +19,14 @@
# 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
@@ -28,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
@@ -93,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)
+
# --------------------------------------------------------------------.
@@ -119,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),
@@ -142,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
@@ -201,7 +281,6 @@ class ImageCmsProfile:
class ImageCmsTransform(Image.ImagePointHandler):
-
"""
Transform. This can be used with the procedural API, or with the standard
:py:func:`~PIL.Image.Image.point` method.
@@ -218,7 +297,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(
@@ -289,7 +368,6 @@ def get_display_profile(handle=None):
class PyCMSError(Exception):
-
"""(pyCMS) Exception class.
This is used for all errors in the pyCMS API."""
@@ -303,7 +381,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
@@ -420,7 +498,7 @@ def buildTransform(
inMode,
outMode,
renderingIntent=Intent.PERCEPTUAL,
- flags=0,
+ flags=Flags.NONE,
):
"""
(pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
@@ -482,7 +560,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:
@@ -505,7 +583,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
@@ -586,7 +664,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:
@@ -1004,4 +1082,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 bfad27c82..5fb80b753 100644
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -19,10 +19,12 @@
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
@@ -121,7 +123,8 @@ def getrgb(color):
raise ValueError(msg)
-def getcolor(color, mode):
+@lru_cache
+def getcolor(color, mode: str) -> tuple[int, ...]:
"""
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
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index 8611dcc36..d4e000087 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -33,8 +33,11 @@ from __future__ import annotations
import math
import numbers
+import struct
+from typing import Sequence, cast
from . import Image, ImageColor
+from ._typing import Coords
"""
A simple 2D drawing interface for PIL images.
@@ -47,7 +50,7 @@ directly.
class ImageDraw:
font = None
- def __init__(self, im, mode=None):
+ def __init__(self, im: Image.Image, mode: str | None = None) -> None:
"""
Create a drawing instance.
@@ -114,7 +117,7 @@ class ImageDraw:
self.font = ImageFont.load_default()
return self.font
- def _getfont(self, font_size):
+ def _getfont(self, font_size: float | None):
if font_size is not None:
from . import ImageFont
@@ -123,7 +126,7 @@ class ImageDraw:
font = self.getfont()
return font
- def _getink(self, ink, fill=None):
+ def _getink(self, ink, fill=None) -> tuple[int | None, int | None]:
if ink is None and fill is None:
if self.fill:
fill = self.ink
@@ -144,13 +147,13 @@ class ImageDraw:
fill = self.draw.draw_ink(fill)
return ink, fill
- def arc(self, xy, start, end, fill=None, width=1):
+ def arc(self, xy: Coords, start, end, fill=None, width=1) -> None:
"""Draw an arc."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_arc(xy, start, end, ink, width)
- def bitmap(self, xy, bitmap, fill=None):
+ def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None:
"""Draw a bitmap."""
bitmap.load()
ink, fill = self._getink(fill)
@@ -159,7 +162,7 @@ class ImageDraw:
if ink is not None:
self.draw.draw_bitmap(xy, bitmap.im, ink)
- def chord(self, xy, start, end, fill=None, outline=None, width=1):
+ def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None:
"""Draw a chord."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@@ -167,7 +170,7 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0:
self.draw.draw_chord(xy, start, end, ink, 0, width)
- def ellipse(self, xy, fill=None, outline=None, width=1):
+ def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None:
"""Draw an ellipse."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@@ -175,20 +178,29 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0:
self.draw.draw_ellipse(xy, ink, 0, width)
- def line(self, xy, fill=None, width=0, joint=None):
+ def line(self, xy: Coords, fill=None, width=0, joint=None) -> None:
"""Draw a line, or a connected sequence of line segments."""
ink = self._getink(fill)[0]
if ink is not None:
self.draw.draw_lines(xy, ink, width)
if joint == "curve" and width > 4:
- if not isinstance(xy[0], (list, tuple)):
- xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)]
- for i in range(1, len(xy) - 1):
- point = xy[i]
+ points: Sequence[Sequence[float]]
+ if isinstance(xy[0], (list, tuple)):
+ points = cast(Sequence[Sequence[float]], xy)
+ else:
+ points = [
+ cast(Sequence[float], tuple(xy[i : i + 2]))
+ for i in range(0, len(xy), 2)
+ ]
+ for i in range(1, len(points) - 1):
+ point = points[i]
angles = [
math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
% 360
- for start, end in ((xy[i - 1], point), (point, xy[i + 1]))
+ for start, end in (
+ (points[i - 1], point),
+ (point, points[i + 1]),
+ )
]
if angles[0] == angles[1]:
# This is a straight line, so no joint is required
@@ -235,7 +247,7 @@ class ImageDraw:
]
self.line(gap_coords, fill, width=3)
- def shape(self, shape, fill=None, outline=None):
+ def shape(self, shape, fill=None, outline=None) -> None:
"""(Experimental) Draw a shape."""
shape.close()
ink, fill = self._getink(outline, fill)
@@ -244,7 +256,9 @@ class ImageDraw:
if ink is not None and ink != fill:
self.draw.draw_outline(shape, ink, 0)
- def pieslice(self, xy, start, end, fill=None, outline=None, width=1):
+ def pieslice(
+ self, xy: Coords, start, end, fill=None, outline=None, width=1
+ ) -> None:
"""Draw a pieslice."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@@ -252,13 +266,13 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0:
self.draw.draw_pieslice(xy, start, end, ink, 0, width)
- def point(self, xy, fill=None):
+ def point(self, xy: Coords, fill=None) -> None:
"""Draw one or more individual pixels."""
ink, fill = self._getink(fill)
if ink is not None:
self.draw.draw_points(xy, ink)
- def polygon(self, xy, fill=None, outline=None, width=1):
+ def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None:
"""Draw a polygon."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@@ -266,7 +280,7 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0:
if width == 1:
self.draw.draw_polygon(xy, ink, 0, width)
- else:
+ elif self.im is not None:
# To avoid expanding the polygon outwards,
# use the fill as a mask
mask = Image.new("1", self.im.size)
@@ -290,12 +304,12 @@ class ImageDraw:
def regular_polygon(
self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1
- ):
+ ) -> None:
"""Draw a regular polygon."""
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
self.polygon(xy, fill, outline, width)
- def rectangle(self, xy, fill=None, outline=None, width=1):
+ def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None:
"""Draw a rectangle."""
ink, fill = self._getink(outline, fill)
if fill is not None:
@@ -304,13 +318,13 @@ class ImageDraw:
self.draw.draw_rectangle(xy, ink, 0, width)
def rounded_rectangle(
- self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None
- ):
+ self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None
+ ) -> None:
"""Draw a rounded rectangle."""
if isinstance(xy[0], (list, tuple)):
- (x0, y0), (x1, y1) = xy
+ (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy)
else:
- x0, y0, x1, y1 = xy
+ x0, y0, x1, y1 = cast(Sequence[float], xy)
if x1 < x0:
msg = "x1 must be greater than or equal to x0"
raise ValueError(msg)
@@ -345,7 +359,8 @@ class ImageDraw:
r = d // 2
ink, fill = self._getink(outline, fill)
- def draw_corners(pieslice):
+ def draw_corners(pieslice) -> None:
+ parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
if full_x:
# Draw top and bottom halves
parts = (
@@ -360,17 +375,18 @@ class ImageDraw:
)
else:
# Draw four separate corners
- parts = []
- for i, part in enumerate(
- (
- ((x0, y0, x0 + d, y0 + d), 180, 270),
- ((x1 - d, y0, x1, y0 + d), 270, 360),
- ((x1 - d, y1 - d, x1, y1), 0, 90),
- ((x0, y1 - d, x0 + d, y1), 90, 180),
+ parts = tuple(
+ part
+ for i, part in enumerate(
+ (
+ ((x0, y0, x0 + d, y0 + d), 180, 270),
+ ((x1 - d, y0, x1, y0 + d), 270, 360),
+ ((x1 - d, y1 - d, x1, y1), 0, 90),
+ ((x0, y1 - d, x0 + d, y1), 90, 180),
+ )
)
- ):
- if corners[i]:
- parts.append(part)
+ if corners[i]
+ )
for part in parts:
if pieslice:
self.draw.draw_pieslice(*(part + (fill, 1)))
@@ -430,12 +446,12 @@ class ImageDraw:
right[3] -= r + 1
self.draw.draw_rectangle(right, ink, 1)
- def _multiline_check(self, text):
+ def _multiline_check(self, text) -> bool:
split_character = "\n" if isinstance(text, str) else b"\n"
return split_character in text
- def _multiline_split(self, text):
+ def _multiline_split(self, text) -> list[str | bytes]:
split_character = "\n" if isinstance(text, str) else b"\n"
return text.split(split_character)
@@ -464,7 +480,7 @@ class ImageDraw:
embedded_color=False,
*args,
**kwargs,
- ):
+ ) -> None:
"""Draw text."""
if embedded_color and self.mode not in ("RGB", "RGBA"):
msg = "Embedded color supported only in RGB and RGBA modes"
@@ -496,7 +512,7 @@ class ImageDraw:
return fill
return ink
- def draw_text(ink, stroke_width=0, stroke_offset=None):
+ def draw_text(ink, stroke_width=0, stroke_offset=None) -> None:
mode = self.fontmode
if stroke_width == 0 and embedded_color:
mode = "RGBA"
@@ -519,7 +535,7 @@ class ImageDraw:
*args,
**kwargs,
)
- coord = coord[0] + offset[0], coord[1] + offset[1]
+ coord = [coord[0] + offset[0], coord[1] + offset[1]]
except AttributeError:
try:
mask = font.getmask(
@@ -538,14 +554,18 @@ class ImageDraw:
except TypeError:
mask = font.getmask(text)
if stroke_offset:
- coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]
+ coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]]
if mode == "RGBA":
# 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)
+ if self.im is not None:
+ self.im.paste(
+ color, (x, y, x + mask.size[0], y + mask.size[1]), mask
+ )
else:
self.draw.draw_bitmap(coord, mask, ink)
@@ -582,7 +602,7 @@ class ImageDraw:
embedded_color=False,
*,
font_size=None,
- ):
+ ) -> None:
if direction == "ttb":
msg = "ttb direction is unsupported for multiline text"
raise ValueError(msg)
@@ -691,7 +711,7 @@ class ImageDraw:
embedded_color=False,
*,
font_size=None,
- ):
+ ) -> tuple[int, int, int, int]:
"""Get the bounding box of a given string, in pixels."""
if embedded_color and self.mode not in ("RGB", "RGBA"):
msg = "Embedded color supported only in RGB and RGBA modes"
@@ -736,7 +756,7 @@ class ImageDraw:
embedded_color=False,
*,
font_size=None,
- ):
+ ) -> tuple[int, int, int, int]:
if direction == "ttb":
msg = "ttb direction is unsupported for multiline text"
raise ValueError(msg)
@@ -775,7 +795,7 @@ class ImageDraw:
elif anchor[1] == "d":
top -= (len(lines) - 1) * line_spacing
- bbox = None
+ bbox: tuple[int, int, int, int] | None = None
for idx, line in enumerate(lines):
left = xy[0]
@@ -826,7 +846,7 @@ class ImageDraw:
return bbox
-def Draw(im, mode=None):
+def Draw(im, mode: str | None = None) -> ImageDraw:
"""
A simple 2D drawing interface for PIL images.
@@ -874,7 +894,7 @@ def getdraw(im=None, hints=None):
return im, handler
-def floodfill(image, xy, value, border=None, thresh=0):
+def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
"""
(experimental) Fills a bounded region with a given color.
@@ -930,7 +950,9 @@ def floodfill(image, xy, value, border=None, thresh=0):
edge = new_edge
-def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
+def _compute_regular_polygon_vertices(
+ bounding_circle, n_sides, rotation
+) -> list[tuple[float, float]]:
"""
Generate a list of vertices for a 2D regular polygon.
@@ -980,7 +1002,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
# 1.2 Check `bounding_circle` has an appropriate value
if not isinstance(bounding_circle, (list, tuple)):
- msg = "bounding_circle should be a tuple"
+ msg = "bounding_circle should be a sequence"
raise TypeError(msg)
if len(bounding_circle) == 3:
@@ -1012,7 +1034,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
raise ValueError(msg)
# 2. Define Helper Functions
- def _apply_rotation(point, degrees, centroid):
+ def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]:
return (
round(
point[0] * math.cos(math.radians(360 - degrees))
@@ -1028,11 +1050,11 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
),
)
- def _compute_polygon_vertex(centroid, polygon_radius, angle):
+ def _compute_polygon_vertex(angle: float) -> tuple[int, int]:
start_point = [polygon_radius, 0]
- return _apply_rotation(start_point, angle, centroid)
+ return _apply_rotation(start_point, angle)
- def _get_angles(n_sides, rotation):
+ def _get_angles(n_sides: int, rotation: float) -> list[float]:
angles = []
degrees = 360 / n_sides
# Start with the bottom left polygon vertex
@@ -1048,12 +1070,10 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
angles = _get_angles(n_sides, rotation)
# 4. Compute Vertices
- return [
- _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles
- ]
+ return [_compute_polygon_vertex(angle) for angle in angles]
-def _color_diff(color1, color2):
+def _color_diff(color1, color2: float | tuple[int, ...]) -> float:
"""
Uses 1-norm distance to calculate difference between two values.
"""
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index ae4e23db1..e929b665e 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -32,7 +32,7 @@ import io
import itertools
import struct
import sys
-from typing import NamedTuple
+from typing import IO, Any, NamedTuple
from . import Image
from ._deprecate import deprecate
@@ -91,10 +91,10 @@ def _tilesort(t):
class _Tile(NamedTuple):
- encoder_name: str
+ codec_name: str
extents: tuple[int, int, int, int]
offset: int
- args: tuple | str | None
+ args: tuple[Any, ...] | str | None
#
@@ -514,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.
@@ -616,6 +616,8 @@ class PyCodecState:
class PyCodec:
+ fd: IO[bytes] | None
+
def __init__(self, mode, *args):
self.im = None
self.state = PyCodecState()
@@ -713,7 +715,7 @@ class PyDecoder(PyCodec):
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
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 021b40c0e..035b83c4d 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -396,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 6db7cc4ec..256c581df 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -33,10 +33,10 @@ import sys
import warnings
from enum import IntEnum
from io import BytesIO
-from pathlib import Path
-from typing import IO
+from typing import BinaryIO
from . import Image
+from ._typing import StrOrBytesPath
from ._util import is_directory, is_path
@@ -53,7 +53,7 @@ try:
except ImportError as ex:
from ._util import DeferredError
- core = DeferredError(ex)
+ core = DeferredError.new(ex)
def _string_length_check(text):
@@ -149,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):
@@ -191,7 +193,7 @@ class FreeTypeFont:
def __init__(
self,
- font: bytes | str | Path | IO | None = None,
+ font: StrOrBytesPath | BinaryIO | None = None,
size: float = 10,
index: int = 0,
encoding: str = "",
@@ -228,8 +230,7 @@ class FreeTypeFont:
)
if is_path(font):
- if isinstance(font, Path):
- font = str(font)
+ font = os.path.realpath(os.fspath(font))
if sys.platform == "win32":
font_bytes_path = font if isinstance(font, bytes) else font.encode()
try:
@@ -582,22 +583,13 @@ class FreeTypeFont:
_string_length_check(text)
if start is None:
start = (0, 0)
- im = None
- size = None
def fill(width, height):
- nonlocal im, size
-
size = (width, height)
- if Image.MAX_IMAGE_PIXELS is not None:
- pixels = max(1, width) * max(1, height)
- if pixels > 2 * Image.MAX_IMAGE_PIXELS:
- return
+ Image._decompression_bomb_check(size)
+ return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
- im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
- return im
-
- offset = self.font.render(
+ return self.font.render(
text,
fill,
mode,
@@ -610,8 +602,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
@@ -881,7 +871,7 @@ def load_path(filename):
raise OSError(msg)
-def load_default(size=None):
+def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""If FreeType support is available, load a version of Aileron Regular,
https://dotcolon.net/font/aileron, with a more limited character set.
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index a4993d3d4..3f3be706d 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -149,18 +149,7 @@ def grabclipboard():
session_type = None
if shutil.which("wl-paste") and session_type in ("wayland", None):
- output = subprocess.check_output(["wl-paste", "-l"]).decode()
- mimetypes = output.splitlines()
- if "image/png" in mimetypes:
- mimetype = "image/png"
- elif mimetypes:
- mimetype = mimetypes[0]
- else:
- mimetype = None
-
- args = ["wl-paste"]
- if mimetype:
- args.extend(["-t", mimetype])
+ args = ["wl-paste", "-t", "image"]
elif shutil.which("xclip") and session_type in ("x11", None):
args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
else:
@@ -168,10 +157,29 @@ def grabclipboard():
raise NotImplementedError(msg)
p = subprocess.run(args, capture_output=True)
- err = p.stderr
- if err:
- msg = f"{args[0]} error: {err.strip().decode()}"
+ if p.returncode != 0:
+ err = p.stderr
+ for silent_error in [
+ # wl-paste, when the clipboard is empty
+ b"Nothing is copied",
+ # Ubuntu/Debian wl-paste, when the clipboard is empty
+ b"No selection",
+ # Ubuntu/Debian wl-paste, when an image isn't available
+ b"No suitable type of content copied",
+ # wl-paste or Ubuntu/Debian xclip, when an image isn't available
+ b" not available",
+ # xclip, when an image isn't available
+ b"cannot convert ",
+ # xclip, when the clipboard isn't initialized
+ b"xclip: Error: There is no owner for the ",
+ ]:
+ if silent_error in err:
+ return None
+ msg = f"{args[0]} error"
+ if err:
+ msg += f": {err.strip().decode()}"
raise ChildProcessError(msg)
+
data = io.BytesIO(p.stdout)
im = Image.open(data)
im.load()
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index 7ca512e75..a7652f237 100644
--- a/src/PIL/ImageMath.py
+++ b/src/PIL/ImageMath.py
@@ -17,6 +17,8 @@
from __future__ import annotations
import builtins
+from types import CodeType
+from typing import Any
from . import Image, _imagingmath
@@ -24,10 +26,10 @@ from . import Image, _imagingmath
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.
@@ -45,122 +47,131 @@ class _Operand:
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
@@ -170,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.
@@ -233,7 +249,12 @@ 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 args.items():
@@ -242,7 +263,7 @@ def eval(expression, _dict={}, **kw):
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 54c3d01c4..0b31f6081 100644
--- a/src/PIL/ImageMode.py
+++ b/src/PIL/ImageMode.py
@@ -15,77 +15,82 @@
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": " 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"],
@@ -87,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."""
@@ -135,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
@@ -159,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
@@ -172,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
@@ -182,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:
@@ -190,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
@@ -206,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.
@@ -221,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
@@ -232,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())
@@ -242,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"
@@ -250,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 a9e626b2b..6218c723f 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -21,6 +21,7 @@ from __future__ import annotations
import functools
import operator
import re
+from typing import Protocol, Sequence, cast
from . import ExifTags, Image, ImagePalette
@@ -28,7 +29,7 @@ from . import ExifTags, Image, ImagePalette
# helpers
-def _border(border):
+def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
if isinstance(border, tuple):
if len(border) == 2:
left, top = right, bottom = border
@@ -39,7 +40,7 @@ def _border(border):
return left, top, right, bottom
-def _color(color, mode):
+def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]:
if isinstance(color, str):
from . import ImageColor
@@ -47,7 +48,7 @@ def _color(color, mode):
return color
-def _lut(image, lut):
+def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
if image.mode == "P":
# FIXME: apply to lookup table, not image data
msg = "mode P support coming soon"
@@ -65,7 +66,13 @@ def _lut(image, lut):
# actions
-def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
+def autocontrast(
+ image: Image.Image,
+ cutoff: float | tuple[float, float] = 0,
+ ignore: int | Sequence[int] | None = None,
+ mask: Image.Image | None = None,
+ preserve_tone: bool = False,
+) -> Image.Image:
"""
Maximize (normalize) image contrast. This function calculates a
histogram of the input image (or mask region), removes ``cutoff`` percent of the
@@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
h = histogram[layer : layer + 256]
if ignore is not None:
# get rid of outliers
- try:
+ if isinstance(ignore, int):
h[ignore] = 0
- except TypeError:
- # assume sequence
+ else:
for ix in ignore:
h[ix] = 0
if cutoff:
@@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
for ix in range(256):
n = n + h[ix]
# remove cutoff% pixels from the low end
- cut = n * cutoff[0] // 100
+ cut = int(n * cutoff[0] // 100)
for lo in range(256):
if cut > h[lo]:
cut = cut - h[lo]
@@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
if cut <= 0:
break
# remove cutoff% samples from the high end
- cut = n * cutoff[1] // 100
+ cut = int(n * cutoff[1] // 100)
for hi in range(255, -1, -1):
if cut > h[hi]:
cut = cut - h[hi]
@@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
return _lut(image, lut)
-def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127):
+def colorize(
+ image: Image.Image,
+ black: str | tuple[int, ...],
+ white: str | tuple[int, ...],
+ mid: str | int | tuple[int, ...] | None = None,
+ blackpoint: int = 0,
+ whitepoint: int = 255,
+ midpoint: int = 127,
+) -> Image.Image:
"""
Colorize grayscale image.
This function calculates a color wedge which maps all black pixels in
@@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
# Define colors from arguments
- black = _color(black, "RGB")
- white = _color(white, "RGB")
- if mid is not None:
- mid = _color(mid, "RGB")
+ rgb_black = cast(Sequence[int], _color(black, "RGB"))
+ rgb_white = cast(Sequence[int], _color(white, "RGB"))
+ rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None
# Empty lists for the mapping
red = []
@@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
# Create the low-end values
for i in range(0, blackpoint):
- red.append(black[0])
- green.append(black[1])
- blue.append(black[2])
+ red.append(rgb_black[0])
+ green.append(rgb_black[1])
+ blue.append(rgb_black[2])
# Create the mapping (2-color)
- if mid is None:
+ if rgb_mid is None:
range_map = range(0, whitepoint - blackpoint)
for i in range_map:
- red.append(black[0] + i * (white[0] - black[0]) // len(range_map))
- green.append(black[1] + i * (white[1] - black[1]) // len(range_map))
- blue.append(black[2] + i * (white[2] - black[2]) // len(range_map))
+ red.append(
+ rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
+ )
+ green.append(
+ rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
+ )
+ blue.append(
+ rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
+ )
# Create the mapping (3-color)
else:
@@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
range_map2 = range(0, whitepoint - midpoint)
for i in range_map1:
- red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1))
- green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1))
- blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1))
+ red.append(
+ rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
+ )
+ green.append(
+ rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
+ )
+ blue.append(
+ rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
+ )
for i in range_map2:
- red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2))
- green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2))
- blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2))
+ red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
+ green.append(
+ rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
+ )
+ blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
# Create the high-end values
for i in range(0, 256 - whitepoint):
- red.append(white[0])
- green.append(white[1])
- blue.append(white[2])
+ red.append(rgb_white[0])
+ green.append(rgb_white[1])
+ blue.append(rgb_white[2])
# Return converted image
image = image.convert("RGB")
return _lut(image, red + green + blue)
-def contain(image, size, method=Image.Resampling.BICUBIC):
+def contain(
+ image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
+) -> Image.Image:
"""
Returns a resized version of the image, set to the maximum width and height
within the requested size, while maintaining the original aspect ratio.
@@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
return image.resize(size, resample=method)
-def cover(image, size, method=Image.Resampling.BICUBIC):
+def cover(
+ image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
+) -> Image.Image:
"""
Returns a resized version of the image, so that the requested size is
covered, while maintaining the original aspect ratio.
@@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC):
return image.resize(size, resample=method)
-def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
+def pad(
+ image: Image.Image,
+ size: tuple[int, int],
+ method: int = Image.Resampling.BICUBIC,
+ color: str | int | tuple[int, ...] | None = None,
+ centering: tuple[float, float] = (0.5, 0.5),
+) -> Image.Image:
"""
Returns a resized and padded version of the image, expanded to fill the
requested aspect ratio and size.
@@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5
return out
-def crop(image, border=0):
+def crop(image: Image.Image, border: int = 0) -> Image.Image:
"""
Remove border from image. The same amount of pixels are removed
from all four sides. This function works on all image modes.
@@ -349,7 +386,9 @@ def crop(image, border=0):
return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
-def scale(image, factor, resample=Image.Resampling.BICUBIC):
+def scale(
+ image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
+) -> Image.Image:
"""
Returns a rescaled image by a specific factor given in parameter.
A factor greater than 1 expands the image, between 0 and 1 contracts the
@@ -372,7 +411,19 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC):
return image.resize(size, resample)
-def deform(image, deformer, resample=Image.Resampling.BILINEAR):
+class _SupportsGetMesh(Protocol):
+ def getmesh(
+ self, image: Image.Image
+ ) -> list[
+ tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
+ ]: ...
+
+
+def deform(
+ image: Image.Image,
+ deformer: _SupportsGetMesh,
+ resample: int = Image.Resampling.BILINEAR,
+) -> Image.Image:
"""
Deform the image.
@@ -388,7 +439,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR):
)
-def equalize(image, mask=None):
+def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
"""
Equalize the image histogram. This function applies a non-linear
mapping to the input image, in order to create a uniform
@@ -419,7 +470,11 @@ def equalize(image, mask=None):
return _lut(image, lut)
-def expand(image, border=0, fill=0):
+def expand(
+ image: Image.Image,
+ border: int | tuple[int, ...] = 0,
+ fill: str | int | tuple[int, ...] = 0,
+) -> Image.Image:
"""
Add border to the image
@@ -445,7 +500,13 @@ def expand(image, border=0, fill=0):
return out
-def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
+def fit(
+ image: Image.Image,
+ size: tuple[int, int],
+ method: int = Image.Resampling.BICUBIC,
+ bleed: float = 0.0,
+ centering: tuple[float, float] = (0.5, 0.5),
+) -> Image.Image:
"""
Returns a resized and cropped version of the image, cropped to the
requested aspect ratio and size.
@@ -479,13 +540,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
# kevin@cazabon.com
# https://www.cazabon.com
- # ensure centering is mutable
- centering = list(centering)
+ centering_x, centering_y = centering
- if not 0.0 <= centering[0] <= 1.0:
- centering[0] = 0.5
- if not 0.0 <= centering[1] <= 1.0:
- centering[1] = 0.5
+ if not 0.0 <= centering_x <= 1.0:
+ centering_x = 0.5
+ if not 0.0 <= centering_y <= 1.0:
+ centering_y = 0.5
if not 0.0 <= bleed < 0.5:
bleed = 0.0
@@ -522,8 +582,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
crop_height = live_size[0] / output_ratio
# make the crop
- crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0]
- crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1]
+ crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
+ crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
@@ -531,7 +591,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
return image.resize(size, method, box=crop)
-def flip(image):
+def flip(image: Image.Image) -> Image.Image:
"""
Flip the image vertically (top to bottom).
@@ -541,7 +601,7 @@ def flip(image):
return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
-def grayscale(image):
+def grayscale(image: Image.Image) -> Image.Image:
"""
Convert the image to grayscale.
@@ -551,7 +611,7 @@ def grayscale(image):
return image.convert("L")
-def invert(image):
+def invert(image: Image.Image) -> Image.Image:
"""
Invert (negate) the image.
@@ -562,7 +622,7 @@ def invert(image):
return image.point(lut) if image.mode == "1" else _lut(image, lut)
-def mirror(image):
+def mirror(image: Image.Image) -> Image.Image:
"""
Flip image horizontally (left to right).
@@ -572,7 +632,7 @@ def mirror(image):
return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
-def posterize(image, bits):
+def posterize(image: Image.Image, bits: int) -> Image.Image:
"""
Reduce the number of bits for each color channel.
@@ -585,7 +645,7 @@ def posterize(image, bits):
return _lut(image, lut)
-def solarize(image, threshold=128):
+def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
"""
Invert all pixel values above a threshold.
@@ -602,7 +662,7 @@ def solarize(image, threshold=128):
return _lut(image, lut)
-def exif_transpose(image, *, in_place=False):
+def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
"""
If an image has an EXIF Orientation tag, other than 1, transpose the image
accordingly, and remove the orientation data.
@@ -616,7 +676,7 @@ def exif_transpose(image, *, in_place=False):
"""
image.load()
image_exif = image.getexif()
- orientation = image_exif.get(ExifTags.Base.Orientation)
+ orientation = image_exif.get(ExifTags.Base.Orientation, 1)
method = {
2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.Transpose.ROTATE_180,
@@ -653,3 +713,4 @@ def exif_transpose(image, *, in_place=False):
return transposed_image
elif not in_place:
return image.copy()
+ return None
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index fbcfa309d..770d10025 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import array
+from typing import Sequence
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
@@ -34,11 +35,11 @@ class ImagePalette:
Defaults to an empty palette.
"""
- def __init__(self, mode="RGB", palette=None):
+ def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None:
self.mode = mode
self.rawmode = None # if set, palette contains raw data
self.palette = palette or bytearray()
- self.dirty = None
+ self.dirty: int | None = None
@property
def palette(self):
@@ -127,7 +128,7 @@ class ImagePalette:
raise ValueError(msg) from e
return index
- def getcolor(self, color, image=None):
+ def getcolor(self, color, image=None) -> int:
"""Given an rgb tuple, allocate palette entry.
.. warning:: This method is experimental.
@@ -192,7 +193,7 @@ class ImagePalette:
# Internal
-def raw(rawmode, data):
+def raw(rawmode, data) -> ImagePalette:
palette = ImagePalette()
palette.rawmode = rawmode
palette.palette = data
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index 6377c7501..293ba4941 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -19,19 +19,26 @@ from __future__ import annotations
import sys
from io import BytesIO
+from typing import Callable
from . import Image
from ._util import is_path
+qt_version: str | None
qt_versions = [
["6", "PyQt6"],
["side6", "PySide6"],
]
# If a version has already been imported, attempt it first
-qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True)
-for qt_version, qt_module in qt_versions:
+qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
+for version, qt_module in qt_versions:
try:
+ QBuffer: type
+ QIODevice: type
+ QImage: type
+ QPixmap: type
+ qRgba: Callable[[int, int, int, int], int]
if qt_module == "PyQt6":
from PyQt6.QtCore import QBuffer, QIODevice
from PyQt6.QtGui import QImage, QPixmap, qRgba
@@ -41,6 +48,7 @@ for qt_version, qt_module in qt_versions:
except (ImportError, RuntimeError):
continue
qt_is_installed = True
+ qt_version = version
break
else:
qt_is_installed = False
diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py
index e09b001e8..2c1850276 100644
--- a/src/PIL/ImageSequence.py
+++ b/src/PIL/ImageSequence.py
@@ -16,6 +16,10 @@
##
from __future__ import annotations
+from typing import Callable
+
+from . import Image
+
class Iterator:
"""
@@ -29,14 +33,14 @@ 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
@@ -44,10 +48,10 @@ class Iterator:
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
@@ -57,7 +61,10 @@ class Iterator:
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 fad3e0980..d90545e92 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -13,18 +13,20 @@
#
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::
@@ -49,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.
@@ -69,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.
@@ -87,16 +89,16 @@ 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.
@@ -104,15 +106,15 @@ class Viewer:
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.
"""
@@ -129,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 "
@@ -147,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.
"""
@@ -180,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
+
+ def get_command(self, file: str, **options: Any) -> str:
command = self.get_command_ex(file, **options)[0]
return f"({command} {quote(file)}"
@@ -190,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.
"""
@@ -208,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.
"""
@@ -231,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.
"""
@@ -247,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.
"""
@@ -266,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"
@@ -274,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.
"""
@@ -304,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/ImageTransform.py b/src/PIL/ImageTransform.py
index 1fdaa9140..6aa82dadd 100644
--- a/src/PIL/ImageTransform.py
+++ b/src/PIL/ImageTransform.py
@@ -14,17 +14,29 @@
#
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)
@@ -42,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.
@@ -51,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.
@@ -64,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`.
@@ -80,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
@@ -95,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/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py
index 7469c592d..abb3fb762 100644
--- a/src/PIL/ImtImagePlugin.py
+++ b/src/PIL/ImtImagePlugin.py
@@ -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 e7dc3e4e4..409609434 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -16,30 +16,48 @@
#
from __future__ import annotations
-import os
-import tempfile
+from io import BytesIO
+from typing import Sequence
from . import Image, ImageFile
-from ._binary import i8, o8
from ._binary import i16be as i16
from ._binary import i32be as i32
+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()
@@ -52,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)
@@ -77,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()
@@ -103,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:
@@ -128,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):
@@ -159,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)
@@ -185,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
@@ -221,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/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 59bade303..81b8749a3 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -87,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)
@@ -783,6 +785,7 @@ def _save(im, fp, filename):
progressive,
info.get("smooth", 0),
optimize,
+ info.get("keep_rgb", False),
info.get("streamtype", 0),
dpi[0],
dpi[1],
diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py
index 9ecfdb259..d0e64a35e 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
diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py
index 9a85c0d15..27972236c 100644
--- a/src/PIL/McIdasImagePlugin.py
+++ b/src/PIL/McIdasImagePlugin.py
@@ -22,8 +22,8 @@ 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"
##
@@ -34,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/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py
index f4e598ca3..1565612f8 100644
--- a/src/PIL/MpegImagePlugin.py
+++ b/src/PIL/MpegImagePlugin.py
@@ -16,21 +16,22 @@ from __future__ import annotations
from . import Image, ImageFile
from ._binary import i8
+from ._typing import SupportsRead
#
# Bitstream parser
class BitStream:
- def __init__(self, fp):
+ def __init__(self, fp: SupportsRead[bytes]) -> 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 +41,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 +62,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/MspImagePlugin.py b/src/PIL/MspImagePlugin.py
index 77dac65b6..65cc70624 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -26,6 +26,7 @@ from __future__ import annotations
import io
import struct
+from typing import IO
from . import Image, ImageFile
from ._binary import i16le as i16
@@ -35,7 +36,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"]
@@ -48,8 +49,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"
@@ -109,7 +112,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:
@@ -159,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder)
# write MSP files (uncompressed only)
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if im.mode != "1":
msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg)
diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py
index a0515b302..1cd5c4a9d 100644
--- a/src/PIL/PcdImagePlugin.py
+++ b/src/PIL/PcdImagePlugin.py
@@ -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 d602a1633..0d1968b14 100644
--- a/src/PIL/PcfFontFile.py
+++ b/src/PIL/PcfFontFile.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import io
+from typing import BinaryIO, Callable
from . import FontFile, Image
from ._binary import i8
@@ -41,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,
@@ -49,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)]
@@ -58,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))
@@ -104,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
@@ -119,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
@@ -138,18 +141,16 @@ class PcfFontFile(FontFile.FontFile):
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,7 +183,9 @@ 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
@@ -222,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))
@@ -233,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 98ecefd05..026bfd9a0 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -28,6 +28,7 @@ from __future__ import annotations
import io
import logging
+from typing import IO
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@@ -37,7 +38,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]
@@ -49,8 +50,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"
@@ -141,7 +144,7 @@ SAVE = {
}
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:
@@ -199,6 +202,8 @@ 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))
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
index 3506aadce..1777f1f20 100644
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -188,9 +188,9 @@ def _save(im, fp, filename, save_all=False):
x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0)
info = {
- "title": None
- if is_appending
- else os.path.splitext(os.path.basename(filename))[0],
+ "title": (
+ None if is_appending else os.path.splitext(os.path.basename(filename))[0]
+ ),
"author": None,
"subject": None,
"keywords": None,
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index 014460006..4c5101738 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -8,6 +8,7 @@ import os
import re
import time
import zlib
+from typing import TYPE_CHECKING, Any, List, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@@ -239,12 +240,18 @@ class PdfName:
return bytes(result)
-class PdfArray(list):
+class PdfArray(List[Any]):
def __bytes__(self):
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
-class PdfDict(collections.UserDict):
+if TYPE_CHECKING:
+ _DictBase = collections.UserDict[Union[str, bytes], Any]
+else:
+ _DictBase = collections.UserDict
+
+
+class PdfDict(_DictBase):
def __setattr__(self, key, value):
if key == "data":
collections.UserDict.__setattr__(self, key, value)
diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py
index af866feb3..887b6568b 100644
--- a/src/PIL/PixarImagePlugin.py
+++ b/src/PIL/PixarImagePlugin.py
@@ -27,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"
@@ -39,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 e4ed93880..823f12492 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -378,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"]
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 25dbfa5b0..6ac7a9bbc 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -15,6 +15,9 @@
#
from __future__ import annotations
+import math
+from typing import IO
+
from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
@@ -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: IO[bytes], 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,7 +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))])
+ 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))])
#
@@ -339,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/PyAccess.py b/src/PIL/PyAccess.py
index 23ff154f6..2c831913d 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -25,6 +25,7 @@ import sys
from ._deprecate import deprecate
+FFI: type
try:
from cffi import FFI
@@ -43,7 +44,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__)
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index f9a10f610..7bd84ebd4 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -24,13 +24,14 @@ from __future__ import annotations
import os
import struct
+from typing import IO
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,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile):
]
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode"
raise ValueError(msg)
@@ -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/SunImagePlugin.py b/src/PIL/SunImagePlugin.py
index 11ce3dfef..4e098474a 100644
--- a/src/PIL/SunImagePlugin.py
+++ b/src/PIL/SunImagePlugin.py
@@ -21,7 +21,7 @@ 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 26522d93f..7470663b4 100644
--- a/src/PIL/TarIO.py
+++ b/src/PIL/TarIO.py
@@ -16,14 +16,15 @@
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.
@@ -57,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 65c7484f7..828701342 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import warnings
+from typing import IO
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: IO[bytes], 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 fc242ca64..3ba4de9d1 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -50,6 +50,7 @@ import warnings
from collections.abc import MutableMapping
from fractions import Fraction
from numbers import Number, Rational
+from typing import TYPE_CHECKING, Any, Callable
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
@@ -306,6 +307,13 @@ _load_dispatch = {}
_write_dispatch = {}
+def _delegate(op):
+ def delegate(self, *args):
+ return getattr(self._val, op)(*args)
+
+ return delegate
+
+
class IFDRational(Rational):
"""Implements a rational class where 0/0 is a legal value to match
the in the wild use of exif rationals.
@@ -391,12 +399,6 @@ class IFDRational(Rational):
self._numerator = _numerator
self._denominator = _denominator
- def _delegate(op):
- def delegate(self, *args):
- return getattr(self._val, op)(*args)
-
- return delegate
-
""" a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
'mod','rmod', 'pow','rpow', 'pos', 'neg',
@@ -436,7 +438,50 @@ class IFDRational(Rational):
__int__ = _delegate("__int__")
-class ImageFileDirectory_v2(MutableMapping):
+def _register_loader(idx, size):
+ def decorator(func):
+ from .TiffTags import TYPES
+
+ if func.__name__.startswith("load_"):
+ TYPES[idx] = func.__name__[5:].replace("_", " ")
+ _load_dispatch[idx] = size, func # noqa: F821
+ return func
+
+ return decorator
+
+
+def _register_writer(idx):
+ def decorator(func):
+ _write_dispatch[idx] = func # noqa: F821
+ return func
+
+ return decorator
+
+
+def _register_basic(idx_fmt_name):
+ from .TiffTags import TYPES
+
+ idx, fmt, name = idx_fmt_name
+ TYPES[idx] = name
+ size = struct.calcsize("=" + fmt)
+ _load_dispatch[idx] = ( # noqa: F821
+ size,
+ lambda self, data, legacy_api=True: (
+ self._unpack(f"{len(data) // size}{fmt}", data)
+ ),
+ )
+ _write_dispatch[idx] = lambda self, *values: ( # noqa: F821
+ b"".join(self._pack(fmt, value) for value in values)
+ )
+
+
+if TYPE_CHECKING:
+ _IFDv2Base = MutableMapping[int, Any]
+else:
+ _IFDv2Base = MutableMapping
+
+
+class ImageFileDirectory_v2(_IFDv2Base):
"""This class represents a TIFF tag directory. To speed things up, we
don't decode tags unless they're asked for.
@@ -497,6 +542,9 @@ class ImageFileDirectory_v2(MutableMapping):
"""
+ _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {}
+ _write_dispatch: dict[int, Callable[..., Any]] = {}
+
def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None):
"""Initialize an ImageFileDirectory.
@@ -531,7 +579,10 @@ class ImageFileDirectory_v2(MutableMapping):
prefix = property(lambda self: self._prefix)
offset = property(lambda self: self._offset)
- legacy_api = property(lambda self: self._legacy_api)
+
+ @property
+ def legacy_api(self):
+ return self._legacy_api
@legacy_api.setter
def legacy_api(self, value):
@@ -674,40 +725,6 @@ class ImageFileDirectory_v2(MutableMapping):
def _pack(self, fmt, *values):
return struct.pack(self._endian + fmt, *values)
- def _register_loader(idx, size):
- def decorator(func):
- from .TiffTags import TYPES
-
- if func.__name__.startswith("load_"):
- TYPES[idx] = func.__name__[5:].replace("_", " ")
- _load_dispatch[idx] = size, func # noqa: F821
- return func
-
- return decorator
-
- def _register_writer(idx):
- def decorator(func):
- _write_dispatch[idx] = func # noqa: F821
- return func
-
- return decorator
-
- def _register_basic(idx_fmt_name):
- from .TiffTags import TYPES
-
- idx, fmt, name = idx_fmt_name
- TYPES[idx] = name
- size = struct.calcsize("=" + fmt)
- _load_dispatch[idx] = ( # noqa: F821
- size,
- lambda self, data, legacy_api=True: (
- self._unpack(f"{len(data) // size}{fmt}", data)
- ),
- )
- _write_dispatch[idx] = lambda self, *values: ( # noqa: F821
- b"".join(self._pack(fmt, value) for value in values)
- )
-
list(
map(
_register_basic,
@@ -995,7 +1012,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
tagdata = property(lambda self: self._tagdata)
# defined in ImageFileDirectory_v2
- tagtype: dict
+ tagtype: dict[int, int]
"""Dictionary of tag types"""
@classmethod
@@ -1704,25 +1721,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)
@@ -1833,11 +1852,11 @@ def _save(im, fp, filename):
tags = list(atts.items())
tags.sort()
a = (rawmode, compression, _fp, filename, tags, types)
- e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
- e.setimage(im.im, (0, 0) + im.size)
+ encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
+ encoder.setimage(im.im, (0, 0) + im.size)
while True:
# undone, change to self.decodermaxblock:
- errcode, data = e.encode(16 * 1024)[1:]
+ errcode, data = encoder.encode(16 * 1024)[1:]
if not _fp:
fp.write(data)
if errcode:
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 88ff2f4fc..b94193931 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -22,7 +22,7 @@ from collections import namedtuple
class TagInfo(namedtuple("_TagInfo", "value name type length enum")):
- __slots__ = []
+ __slots__: list[str] = []
def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None):
return super().__new__(cls, value, name, type, length, enum or {})
@@ -437,7 +437,7 @@ _populate()
##
# Map type numbers to type names -- defined in ImageFileDirectory.
-TYPES = {}
+TYPES: dict[int, str] = {}
#
# These tags are handled by default in libtiff, without
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index 47ba1c548..c84adaca2 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -33,7 +33,7 @@ for r in range(8):
)
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:6] == _MAGIC
@@ -45,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 566acbfe5..eee727436 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -21,6 +21,7 @@
from __future__ import annotations
import re
+from typing import IO
from . import Image, ImageFile
@@ -36,7 +37,7 @@ xbm_head = re.compile(
)
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix.lstrip()[:7] == b"#define"
@@ -48,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:
@@ -67,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: IO[bytes], filename: str) -> None:
if im.mode != "1":
msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg)
diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py
index 3fcac8643..63a45769b 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/_binary.py b/src/PIL/_binary.py
index c60c9cec1..0a07e8d0e 100644
--- a/src/PIL/_binary.py
+++ b/src/PIL/_binary.py
@@ -18,16 +18,16 @@ 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/_imaging.pyi b/src/PIL/_imaging.pyi
new file mode 100644
index 000000000..b0235555d
--- /dev/null
+++ b/src/PIL/_imaging.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
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 03a6eba44..beddfb062 100644
--- a/src/PIL/_tkinter_finder.py
+++ b/src/PIL/_tkinter_finder.py
@@ -1,10 +1,12 @@
""" Find compiled module linking to Tcl / Tk libraries
"""
+
from __future__ import annotations
import sys
import tkinter
-from tkinter import _tkinter as tk
+
+tk = getattr(tkinter, "_tkinter")
try:
if hasattr(sys, "pypy_find_executable"):
diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py
new file mode 100644
index 000000000..7075e8672
--- /dev/null
+++ b/src/PIL/_typing.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+import os
+import sys
+from typing import Protocol, Sequence, TypeVar, Union
+
+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
+
+
+Coords = Union[Sequence[float], Sequence[Sequence[float]]]
+
+
+_T_co = TypeVar("_T_co", covariant=True)
+
+
+class SupportsRead(Protocol[_T_co]):
+ def read(self, __length: int = ...) -> _T_co: ...
+
+
+StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
+
+
+__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"]
diff --git a/src/PIL/_util.py b/src/PIL/_util.py
index 4634d335b..6bc762816 100644
--- a/src/PIL/_util.py
+++ b/src/PIL/_util.py
@@ -1,21 +1,31 @@
from __future__ import annotations
import os
-from pathlib import Path
+from typing import Any, NoReturn
+
+from ._typing import StrOrBytesPath, TypeGuard
-def is_path(f):
- return isinstance(f, (bytes, str, Path))
+def is_path(f: Any) -> TypeGuard[StrOrBytesPath]:
+ return isinstance(f, (bytes, str, os.PathLike))
-def is_directory(f):
+def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]:
"""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 7d994caf4..0568943b5 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
-__version__ = "10.2.0.dev0"
+__version__ = "10.3.0.dev0"
diff --git a/src/PIL/_webp.pyi b/src/PIL/_webp.pyi
new file mode 100644
index 000000000..b0235555d
--- /dev/null
+++ b/src/PIL/_webp.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/py.typed b/src/PIL/py.typed
new file mode 100644
index 000000000..e69de29bb
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 4925dc233..6e24fcf95 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) {
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;
@@ -894,7 +894,7 @@ font_render(FontObject *self, PyObject *args) {
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) {
@@ -1049,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 */
@@ -1061,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;
}
}
}
@@ -1109,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/_webp.c b/src/_webp.c
index a1b4dbc1a..47592547c 100644
--- a/src/_webp.c
+++ b/src/_webp.c
@@ -448,11 +448,16 @@ PyObject *
_anim_decoder_get_next(PyObject *self) {
uint8_t *buf;
int timestamp;
+ int ok;
PyObject *bytes;
PyObject *ret;
+ ImagingSectionCookie cookie;
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
- if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) {
+ ImagingSectionEnter(&cookie);
+ ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp);
+ ImagingSectionLeave(&cookie);
+ if (!ok) {
PyErr_SetString(PyExc_OSError, "failed to read next frame");
return NULL;
}
diff --git a/src/encode.c b/src/encode.c
index 4664ad0f3..c7dd51015 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -1042,6 +1042,7 @@ 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 */
@@ -1059,13 +1060,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "ss|nnnnnnnnnnOz#y#y#",
+ "ss|nnnnpnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
&progressive,
&smooth,
&optimize,
+ &keep_rgb,
&streamtype,
&xdpi,
&ydpi,
@@ -1150,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;
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 5cc74e69b..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;
diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c
index 9da830b18..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;
diff --git a/tox.ini b/tox.ini
index 034d89372..85a2020d6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -33,7 +33,16 @@ commands =
[testenv:mypy]
skip_install = true
deps =
- mypy==1.7.1
+ -r .ci/requirements-mypy.txt
+ IceSpringPySideStubs-PyQt6
+ IceSpringPySideStubs-PySide6
+ ipython
numpy
+ packaging
+ types-cffi
+ types-defusedxml
+ types-olefile
+extras =
+ typing
commands =
mypy src {posargs}
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 f7e145fb9..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"},
}
@@ -171,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",
@@ -200,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)": "",
@@ -214,6 +216,7 @@ DEPS = {
*cmds_cmake(
"tiff",
"-DBUILD_SHARED_LIBS:BOOL=OFF",
+ "-DWebP_LIBRARY=libwebp",
'-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"',
)
],
@@ -367,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"],
},
@@ -381,10 +391,9 @@ def find_msvs(architecture: str) -> dict[str, str] | None:
print("Program Files not found")
return None
+ requires = ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"]
if architecture == "ARM64":
- tools = "Microsoft.VisualStudio.Component.VC.Tools.ARM64"
- else:
- tools = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
+ requires += ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.ARM64"]
try:
vspath = (
@@ -395,8 +404,7 @@ def find_msvs(architecture: str) -> dict[str, str] | None:
),
"-latest",
"-prerelease",
- "-requires",
- tools,
+ *requires,
"-property",
"installationPath",
"-products",
@@ -643,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)",
@@ -707,11 +715,6 @@ if __name__ == "__main__":
disabled += ["libimagequant"]
if args.no_fribidi:
disabled += ["fribidi"]
- elif args.architecture == "ARM64" and platform.machine() != "ARM64":
- import warnings
-
- warnings.warn("Cross-compiling FriBiDi is currently not supported, disabling")
- disabled += ["fribidi"]
prefs = {
"architecture": args.architecture,
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")