Merge branch 'main' into build-editable

This commit is contained in:
Andrew Murray 2024-02-15 11:12:17 +11:00 committed by GitHub
commit bf9ba35b20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
282 changed files with 4125 additions and 2951 deletions

View File

@ -14,7 +14,7 @@ environment:
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64 - PYTHON: C:/Python38-x64
ARCHITECTURE: x64 ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017

View File

@ -1 +1 @@
cibuildwheel==2.16.2 cibuildwheel==2.16.5

View File

@ -10,6 +10,11 @@ exclude_also =
if DEBUG: if DEBUG:
# Don't complain about compatibility code for missing optional dependencies # Don't complain about compatibility code for missing optional dependencies
except ImportError 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] [run]
omit = omit =

View File

@ -37,16 +37,26 @@ jobs:
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip cache: pip
cache-dependency-path: ".ci/*.sh" cache-dependency-path: |
".ci/*.sh"
"pyproject.toml"
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py 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 - name: Install Linux dependencies
run: | run: |
.ci/install.sh .ci/install.sh
env: env:
GHA_PYTHON_VERSION: "3.x" GHA_PYTHON_VERSION: "3.x"
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
- name: Build - name: Build
run: | run: |

View File

@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: pre-commit cache - name: pre-commit cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.cache/pre-commit path: ~/.cache/pre-commit
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}

View File

@ -2,7 +2,16 @@
set -e 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" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
# TODO Update condition when cffi supports 3.13 # TODO Update condition when cffi supports 3.13

View File

@ -23,6 +23,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Drafts your next release notes as pull requests are merged into "main" # 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: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -6,6 +6,7 @@ This sort of info is missing from GitHub Actions.
Requested here: Requested here:
https://github.com/actions/virtual-environments/issues/79 https://github.com/actions/virtual-environments/issues/79
""" """
from __future__ import annotations from __future__ import annotations
import os import os

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:
@ -49,9 +47,8 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Cygwin - name: Install Cygwin
uses: cygwin/cygwin-install-action@v4 uses: egor-tensin/setup-cygwin@v4
with: with:
platform: x86_64
packages: > packages: >
gcc-g++ gcc-g++
ghostscript ghostscript
@ -71,6 +68,7 @@ jobs:
make make
netpbm netpbm
perl perl
python39=3.9.16-1
python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-devel
@ -82,13 +80,13 @@ jobs:
zlib-devel zlib-devel
- name: Add Lapack to PATH - name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v3 uses: egor-tensin/cleanup-path@v4
with: with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
- name: Select Python version - name: Select Python version
run: | 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 - name: Get latest NumPy version
id: latest-numpy id: latest-numpy
@ -97,7 +95,7 @@ jobs:
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
- name: pip cache - name: pip cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: 'C:\cygwin\home\runneradmin\.cache\pip' 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') }} key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
@ -143,7 +141,7 @@ jobs:
bash.exe .ci/after_success.sh bash.exe .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Cygwin flags: GHA_Cygwin

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:
@ -103,7 +101,7 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }} MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.docker }} name: ${{ matrix.docker }}

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:
@ -84,7 +82,7 @@ jobs:
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows

View File

@ -2,11 +2,12 @@ name: Test Windows
on: on:
push: push:
branches:
- "**"
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -14,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:
@ -89,7 +89,7 @@ jobs:
- name: Cache build - name: Cache build
id: build-cache id: build-cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: winbuild\build path: winbuild\build
key: key:
@ -202,7 +202,7 @@ jobs:
shell: pwsh shell: pwsh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:
@ -28,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
build: build:
@ -35,7 +36,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ os: [
"macos-latest", "macos-14",
"ubuntu-latest", "ubuntu-latest",
] ]
python-version: [ python-version: [
@ -49,11 +50,21 @@ jobs:
"3.8", "3.8",
] ]
include: include:
- python-version: "3.9" - python-version: "3.11"
PYTHONOPTIMIZE: 1 PYTHONOPTIMIZE: 1
REVERSE: "--reverse" REVERSE: "--reverse"
- python-version: "3.8" - python-version: "3.10"
PYTHONOPTIMIZE: 2 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 }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
@ -67,17 +78,28 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
cache: pip cache: pip
cache-dependency-path: ".ci/*.sh" cache-dependency-path: |
".ci/*.sh"
"pyproject.toml"
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py 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 - name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu') if: startsWith(matrix.os, 'ubuntu')
run: | run: |
.ci/install.sh .ci/install.sh
env: env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
- name: Install macOS dependencies - name: Install macOS dependencies
if: startsWith(matrix.os, 'macOS') if: startsWith(matrix.os, 'macOS')
@ -127,9 +149,9 @@ jobs:
.ci/after_success.sh .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: 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 }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true gcov: true

View File

@ -72,13 +72,11 @@ function build {
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then if [ -n "$IS_MACOS" ]; then
if [[ "$CIBW_ARCHS" == "arm64" ]]; then build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
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 libXau 1.0.11 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
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
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
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
fi
fi fi
else else
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc 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 if [[ -n "$IS_MACOS" ]]; then
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb # 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 # if php is installed, brew tries to reinstall these after installing openblas
# remove cairo to fix building harfbuzz on arm64 # remove cairo to fix building harfbuzz on arm64
# remove lcms2 and libpng to fix building openjpeg on arm64 # remove lcms2 and libpng to fix building openjpeg on arm64
# remove zstd to avoid inclusion on x86_64 # remove zstd to avoid inclusion on x86_64
# curl from brew requires zstd, use system curl # 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 brew install pkg-config
fi fi

View File

@ -32,7 +32,64 @@ env:
FORCE_COLOR: 1 FORCE_COLOR: 1
jobs: 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 }} name: ${{ matrix.name }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -41,18 +98,18 @@ jobs:
include: include:
- name: "macOS x86_64" - name: "macOS x86_64"
os: macos-latest os: macos-latest
archs: x86_64 cibw_arch: x86_64
macosx_deployment_target: "10.10" macosx_deployment_target: "10.10"
- name: "macOS arm64" - name: "macOS arm64"
os: macos-latest os: macos-latest
archs: arm64 cibw_arch: arm64
macosx_deployment_target: "11.0" macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64" - name: "manylinux2014 and musllinux x86_64"
os: ubuntu-latest os: ubuntu-latest
archs: x86_64 cibw_arch: x86_64
- name: "manylinux_2_28 x86_64" - name: "manylinux_2_28 x86_64"
os: ubuntu-latest os: ubuntu-latest
archs: x86_64 cibw_arch: x86_64
build: "*manylinux*" build: "*manylinux*"
manylinux: "manylinux_2_28" manylinux: "manylinux_2_28"
steps: steps:
@ -64,12 +121,15 @@ jobs:
with: with:
python-version: "3.x" python-version: "3.x"
- name: Build wheels - name: Install cibuildwheel
run: | run: |
python3 -m pip install -r .ci/requirements-cibw.txt python3 -m pip install -r .ci/requirements-cibw.txt
- name: Build wheels
run: |
python3 -m cibuildwheel --output-dir wheelhouse python3 -m cibuildwheel --output-dir wheelhouse
env: env:
CIBW_ARCHS: ${{ matrix.archs }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }} CIBW_BUILD: ${{ matrix.build }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
@ -79,22 +139,19 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: dist-${{ matrix.os }}-${{ matrix.archs }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
windows: windows:
name: Windows ${{ matrix.arch }} name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- arch: x86 - cibw_arch: x86
cibw_arch: x86 - cibw_arch: AMD64
- arch: x64 - cibw_arch: ARM64
cibw_arch: AMD64
- arch: ARM64
cibw_arch: ARM64
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -108,6 +165,10 @@ jobs:
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install cibuildwheel
run: |
python.exe -m pip install -r .ci/requirements-cibw.txt
- name: Prepare for build - name: Prepare for build
run: | run: |
choco install nasm --no-progress choco install nasm --no-progress
@ -116,9 +177,7 @@ jobs:
# Install extra test images # Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images xcopy /S /Y Tests\test-images\* Tests\images
& python.exe -m pip install -r .ci/requirements-cibw.txt & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }}
shell: pwsh shell: pwsh
- name: Build wheels - name: Build wheels
@ -159,13 +218,13 @@ jobs:
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: dist-windows-${{ matrix.arch }} name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
- name: Upload fribidi.dll - name: Upload fribidi.dll
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: fribidi-windows-${{ matrix.arch }} name: fribidi-windows-${{ matrix.cibw_arch }}
path: winbuild\build\bin\fribidi* path: winbuild\build\bin\fribidi*
sdist: sdist:
@ -189,7 +248,7 @@ jobs:
pypi-publish: pypi-publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: [build, windows, sdist] needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Upload release to PyPI name: Upload release to PyPI
environment: environment:

View File

@ -1,17 +1,17 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.9 rev: v0.2.0
hooks: hooks:
- id: ruff - id: ruff
args: [--fix, --exit-non-zero-on-fix] args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.1 rev: 24.1.1
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.6 rev: 1.7.7
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -32,6 +32,7 @@ repos:
rev: v4.5.0 rev: v4.5.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json - id: check-json
- id: check-toml - id: check-toml
@ -47,12 +48,12 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.5.3 rev: 1.7.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.15 rev: v0.16
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject

View File

@ -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

View File

@ -2,6 +2,39 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
10.3.0 (unreleased)
-------------------
- 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) 10.2.0 (2024-01-02)
------------------- -------------------

View File

@ -48,9 +48,6 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
alt="GitHub Actions build status (Wheels)" alt="GitHub Actions build status (Wheels)"
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
<a href="https://app.travis-ci.com/github/python-pillow/Pillow"><img
alt="Travis CI wheels build status (aarch64)"
src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels"></a>
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img <a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage" alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a> src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
@ -68,10 +65,10 @@ As of 2019, Pillow development is
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img <a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
alt="Tidelift" alt="Tidelift"
src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a> src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a>
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/pillow/"><img
alt="Newest PyPI version" alt="Newest PyPI version"
src="https://img.shields.io/pypi/v/pillow.svg"></a> src="https://img.shields.io/pypi/v/pillow.svg"></a>
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/pillow/"><img
alt="Number of PyPI downloads" alt="Number of PyPI downloads"
src="https://img.shields.io/pypi/dm/pillow.svg"></a> src="https://img.shields.io/pypi/dm/pillow.svg"></a>
<a href="https://www.bestpractices.dev/projects/6331"><img <a href="https://www.bestpractices.dev/projects/6331"><img

View File

@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch. * [ ] Develop and prepare release in `main` branch.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them. * [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`. * [ ] Update `CHANGES.rst`.
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
@ -83,12 +83,6 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag. by the new tag.
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
and copy into `dist`. Check and upload them e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/pillow-5.2.0*
```
## Publicize Release ## Publicize Release

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import time import time
from PIL import PyAccess from PIL import PyAccess
@ -8,21 +9,21 @@ from .helper import hopper
# Not running this test by default. No DOS against CI. # Not running this test by default. No DOS against CI.
def iterate_get(size, access): def iterate_get(size, access) -> None:
(w, h) = size (w, h) = size
for x in range(w): for x in range(w):
for y in range(h): for y in range(h):
access[(x, y)] access[(x, y)]
def iterate_set(size, access): def iterate_set(size, access) -> None:
(w, h) = size (w, h) = size
for x in range(w): for x in range(w):
for y in range(h): for y in range(h):
access[(x, y)] = (x % 256, y % 256, 0) access[(x, y)] = (x % 256, y % 256, 0)
def timer(func, label, *args): def timer(func, label, *args) -> None:
iterations = 5000 iterations = 5000
starttime = time.time() starttime = time.time()
for x in range(iterations): for x in range(iterations):
@ -37,7 +38,7 @@ def timer(func, label, *args):
) )
def test_direct(): def test_direct() -> None:
im = hopper() im = hopper()
im.load() im.load()
# im = Image.new("RGB", (2000, 2000), (1, 3, 2)) # im = Image.new("RGB", (2000, 2000), (1, 3, 2))

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image
TEST_FILE = "Tests/images/fli_overflow.fli" 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 # this should not crash with a malloc error or access violation
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()

View File

@ -1,5 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable
import pytest import pytest
from PIL import Image from PIL import Image
@ -12,31 +15,34 @@ max_iterations = 10000
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") 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 from resource import RUSAGE_SELF, getpagesize, getrusage
mem = getrusage(RUSAGE_SELF).ru_maxrss mem = getrusage(RUSAGE_SELF).ru_maxrss
return mem * getpagesize() / 1024 / 1024 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 mem_limit = None
for i in range(max_iterations): for i in range(max_iterations):
fn(*args, **kwargs) fn(*args)
mem = _get_mem_usage() mem = _get_mem_usage()
if i < min_iterations: if i < min_iterations:
mem_limit = mem + 1 mem_limit = mem + 1
continue continue
msg = f"memory usage limit exceeded after {i + 1} iterations" msg = f"memory usage limit exceeded after {i + 1} iterations"
assert mem_limit is not None
assert mem <= mem_limit, msg assert mem <= mem_limit, msg
def test_leak_putdata(): def test_leak_putdata() -> None:
im = Image.new("RGB", (25, 25)) im = Image.new("RGB", (25, 25))
_test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) _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)) im = Image.new("P", (25, 25))
_test_leak( _test_leak(
min_iterations, min_iterations,

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -19,7 +20,7 @@ pytestmark = [
] ]
def test_leak_load(): def test_leak_load() -> None:
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size)) setrlimit(RLIMIT_STACK, (stack_size, stack_size))
@ -29,7 +30,7 @@ def test_leak_load():
im.load() im.load()
def test_leak_save(): def test_leak_save() -> None:
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size)) setrlimit(RLIMIT_STACK, (stack_size, stack_size))

View File

@ -1,10 +1,13 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import Image from PIL import Image
def test_j2k_overflow(tmp_path): def test_j2k_overflow(tmp_path: Path) -> None:
im = Image.new("RGBA", (1024, 131584)) im = Image.new("RGBA", (1024, 131584))
target = str(tmp_path / "temp.jpc") target = str(tmp_path / "temp.jpc")
with pytest.raises(OSError): with pytest.raises(OSError):

3
Tests/check_jp2_overflow.py Executable file → Normal file
View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# Reproductions/tests for OOB read errors in FliDecode.c # Reproductions/tests for OOB read errors in FliDecode.c
# When run in python, all of these images should fail for # When run in python, all of these images should fail for
@ -14,7 +12,6 @@
# version. # version.
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image
repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2")

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -110,14 +111,14 @@ standard_chrominance_qtable = (
[standard_l_qtable, 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") im = hopper("RGB")
for _ in range(iterations): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables) im.save(test_output, "JPEG", qtables=qtables)
def test_exif_leak(): def test_exif_leak() -> None:
""" """
pre patch: pre patch:
@ -180,7 +181,7 @@ def test_exif_leak():
im.save(test_output, "JPEG", exif=exif) im.save(test_output, "JPEG", exif=exif)
def test_base_save(): def test_base_save() -> None:
""" """
base case: base case:
MB MB

View File

@ -1,5 +1,8 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from pathlib import Path
from types import ModuleType
import pytest import pytest
@ -15,6 +18,7 @@ from PIL import Image
# 2.7 and 3.2. # 2.7 and 3.2.
numpy: ModuleType | None
try: try:
import numpy import numpy
except ImportError: except ImportError:
@ -27,23 +31,24 @@ XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") 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") f = str(tmp_path / "temp.png")
im = Image.new("L", (xdim, ydim), 0) im = Image.new("L", (xdim, ydim), 0)
im.save(f) im.save(f)
def test_large(tmp_path): def test_large(tmp_path: Path) -> None:
"""succeeded prepatch""" """succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM) _write_png(tmp_path, XDIM, YDIM)
def test_2gpx(tmp_path): def test_2gpx(tmp_path: Path) -> None:
"""failed prepatch""" """failed prepatch"""
_write_png(tmp_path, XDIM, XDIM) _write_png(tmp_path, XDIM, XDIM)
@pytest.mark.skipif(numpy is None, reason="Numpy is not installed") @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)) arr = numpy.ndarray(shape=(16394, 16394))
Image.fromarray(arr) Image.fromarray(arr)

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from pathlib import Path
import pytest import pytest
@ -23,7 +25,7 @@ XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") 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 dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype) a = np.zeros((xdim, ydim), dtype=dtype)
f = str(tmp_path / "temp.png") f = str(tmp_path / "temp.png")
@ -31,11 +33,11 @@ def _write_png(tmp_path, xdim, ydim):
im.save(f) im.save(f)
def test_large(tmp_path): def test_large(tmp_path: Path) -> None:
"""succeeded prepatch""" """succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM) _write_png(tmp_path, XDIM, YDIM)
def test_2gpx(tmp_path): def test_2gpx(tmp_path: Path) -> None:
"""failed prepatch""" """failed prepatch"""
_write_png(tmp_path, XDIM, XDIM) _write_png(tmp_path, XDIM, XDIM)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image from PIL import Image
@ -6,7 +7,7 @@ from PIL import Image
TEST_FILE = "Tests/images/libtiff_segfault.tif" 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 """This test should not segfault. It will on Pillow <= 3.1.0 and
libtiff >= 4.0.0 libtiff >= 4.0.0
""" """

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import zlib import zlib
from io import BytesIO from io import BytesIO
@ -7,7 +8,7 @@ from PIL import Image, ImageFile, PngImagePlugin
TEST_FILE = "Tests/images/png_decompression_dos.png" TEST_FILE = "Tests/images/png_decompression_dos.png"
def test_ignore_dos_text(): def test_ignore_dos_text() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
try: try:
@ -23,7 +24,7 @@ def test_ignore_dos_text():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_text(): def test_dos_text() -> None:
try: try:
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
@ -35,7 +36,7 @@ def test_dos_text():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" 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)) im = Image.new("L", (1, 1))
compressed_data = zlib.compress(b"a" * 1024 * 1023) compressed_data = zlib.compress(b"a" * 1024 * 1023)
@ -52,7 +53,7 @@ def test_dos_total_memory():
try: try:
im2 = Image.open(b) im2 = Image.open(b)
except ValueError as msg: except ValueError as msg:
assert "Too much memory" in msg assert "Too much memory" in str(msg)
return return
total_len = 0 total_len = 0

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from pathlib import Path from pathlib import Path

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from PIL import features from PIL import features
def test_wheel_modules(): def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
# tkinter is not available in cibuildwheel installed CPython on Windows # 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 assert set(features.get_supported_modules()) == expected_modules
def test_wheel_codecs(): def test_wheel_codecs() -> None:
expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
assert set(features.get_supported_codecs()) == expected_codecs assert set(features.get_supported_codecs()) == expected_codecs
def test_wheel_features(): def test_wheel_features() -> None:
expected_features = { expected_features = {
"webp_anim", "webp_anim",
"webp_mux", "webp_mux",

View File

@ -1,8 +1,11 @@
from __future__ import annotations from __future__ import annotations
import io import io
import pytest
def pytest_report_header(config):
def pytest_report_header(config: pytest.Config) -> str:
try: try:
from PIL import features from PIL import features
@ -13,7 +16,7 @@ def pytest_report_header(config):
return f"pytest_report_header failed: {e}" return f"pytest_report_header failed: {e}"
def pytest_configure(config): def pytest_configure(config: pytest.Config) -> None:
config.addinivalue_line( config.addinivalue_line(
"markers", "markers",
"pil_noop_mark: A conditional mark where nothing special happens", "pil_noop_mark: A conditional mark where nothing special happens",

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import base64 import base64
import os import os

View File

@ -1,6 +1,7 @@
""" """
Helper functions. Helper functions.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
@ -11,6 +12,7 @@ import sys
import sysconfig import sysconfig
import tempfile import tempfile
from io import BytesIO from io import BytesIO
from typing import Any, Callable, Sequence
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -19,42 +21,31 @@ from PIL import Image, ImageMath, features
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
uploader = None
HAS_UPLOADER = False
if os.environ.get("SHOW_ERRORS"): if os.environ.get("SHOW_ERRORS"):
# local img.show for errors. uploader = "show"
HAS_UPLOADER = True
class test_image_results:
@staticmethod
def upload(a, b):
a.show()
b.show()
elif "GITHUB_ACTIONS" in os.environ: elif "GITHUB_ACTIONS" in os.environ:
HAS_UPLOADER = True uploader = "github_actions"
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
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 new_a, new_b = a, b
if a.mode == "P": if a.mode == "P":
new_a = Image.new("L", a.size) new_a = Image.new("L", a.size)
@ -67,14 +58,18 @@ def convert_to_comparable(a, b):
return new_a, new_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: try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception: except Exception:
assert a == b, msg 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: if mode is not None:
assert im.mode == mode, ( assert im.mode == mode, (
msg or f"got mode {repr(im.mode)}, expected {repr(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.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)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
if a.tobytes() != b.tobytes(): if a.tobytes() != b.tobytes():
if HAS_UPLOADER: try:
try: url = upload(a, b)
url = test_image_results.upload(a, b) if url:
logger.error("URL for test images: %s", url) logger.error("URL for test images: %s", url)
except Exception: except Exception:
pass pass
pytest.fail(msg or "got different content") pytest.fail(msg or "got different content")
def assert_image_equal_tofile(a, filename, msg=None, mode=None): def assert_image_equal_tofile(
a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode: if mode:
img = img.convert(mode) img = img.convert(mode)
assert_image_equal(a, img, msg) 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.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)}" 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}" + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}"
) )
except Exception as e: except Exception as e:
if HAS_UPLOADER: try:
try: url = upload(a, b)
url = test_image_results.upload(a, b) if url:
logger.exception("URL for test images: %s", url) logger.exception("URL for test images: %s", url)
except Exception: except Exception:
pass pass
raise e 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: with Image.open(filename) as img:
if mode: if mode:
img = img.convert(mode) img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg) 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 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 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""" """Tests if actuals has values within threshold from targets"""
value = True
for i, target in enumerate(targets): for i, target in enumerate(targets):
value *= target - threshold <= actuals[i] <= target + threshold if not (target - threshold <= actuals[i] <= target + threshold):
pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
assert value, msg + ": " + repr(actuals) + " != " + repr(targets)
def skip_unless_feature(feature): def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
reason = f"{feature} not available" reason = f"{feature} not available"
return pytest.mark.skipif(not features.check(feature), reason=reason) return pytest.mark.skipif(not features.check(feature), reason=reason)
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): if not features.check(feature):
return pytest.mark.skip(f"{feature} not available") return pytest.mark.skip(f"{feature} not available")
if reason is None: if reason is None:
reason = f"{feature} is older than {version_required}" reason = f"{feature} is older than {required}"
version_required = parse_version(version_required) version_required = parse_version(required)
version_available = parse_version(features.version(feature)) version_available = parse_version(features.version(feature))
return pytest.mark.skipif(version_available < version_required, reason=reason) 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): if not features.check(feature):
return pytest.mark.pil_noop_mark() return pytest.mark.pil_noop_mark()
if reason is None: if reason is None:
@ -194,7 +206,7 @@ class PillowLeakTestCase:
iterations = 100 # count iterations = 100 # count
mem_limit = 512 # k 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 Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
between macOS and Linux rss reporting between macOS and Linux rss reporting
@ -216,7 +228,7 @@ class PillowLeakTestCase:
# This is the maximum resident set size used (in kilobytes). # This is the maximum resident set size used (in kilobytes).
return mem # Kb return mem # Kb
def _test_leak(self, core): def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage() start_mem = self._get_mem_usage()
for cycle in range(self.iterations): for cycle in range(self.iterations):
core() core()
@ -228,17 +240,17 @@ class PillowLeakTestCase:
# helpers # helpers
def fromstring(data): def fromstring(data: bytes) -> Image.Image:
return Image.open(BytesIO(data)) return Image.open(BytesIO(data))
def tostring(im, string_format, **options): def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
out = BytesIO() out = BytesIO()
im.save(out, string_format, **options) im.save(out, string_format, **options)
return out.getvalue() 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: if mode is None:
# Always return fresh not-yet-loaded version of image. # Always return fresh not-yet-loaded version of image.
# Operations on not-yet-loaded images is separate class of errors # Operations on not-yet-loaded images is separate class of errors
@ -259,29 +271,31 @@ def hopper(mode=None, cache={}):
return im.copy() return im.copy()
def djpeg_available(): def djpeg_available() -> bool:
if shutil.which("djpeg"): if shutil.which("djpeg"):
try: try:
subprocess.check_call(["djpeg", "-version"]) subprocess.check_call(["djpeg", "-version"])
return True return True
except subprocess.CalledProcessError: # pragma: no cover except subprocess.CalledProcessError: # pragma: no cover
return False return False
return False
def cjpeg_available(): def cjpeg_available() -> bool:
if shutil.which("cjpeg"): if shutil.which("cjpeg"):
try: try:
subprocess.check_call(["cjpeg", "-version"]) subprocess.check_call(["cjpeg", "-version"])
return True return True
except subprocess.CalledProcessError: # pragma: no cover except subprocess.CalledProcessError: # pragma: no cover
return False return False
return False
def netpbm_available(): def netpbm_available() -> bool:
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
def magick_command(): def magick_command() -> list[str] | None:
if sys.platform == "win32": if sys.platform == "win32":
magickhome = os.environ.get("MAGICK_HOME") magickhome = os.environ.get("MAGICK_HOME")
if magickhome: if magickhome:
@ -298,47 +312,48 @@ def magick_command():
return imagemagick return imagemagick
if graphicsmagick and shutil.which(graphicsmagick[0]): if graphicsmagick and shutil.which(graphicsmagick[0]):
return graphicsmagick return graphicsmagick
return None
def on_appveyor(): def on_appveyor() -> bool:
return "APPVEYOR" in os.environ return "APPVEYOR" in os.environ
def on_github_actions(): def on_github_actions() -> bool:
return "GITHUB_ACTIONS" in os.environ return "GITHUB_ACTIONS" in os.environ
def on_ci(): def on_ci() -> bool:
# GitHub Actions and AppVeyor have "CI" # GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ return "CI" in os.environ
def is_big_endian(): def is_big_endian() -> bool:
return sys.byteorder == "big" return sys.byteorder == "big"
def is_ppc64le(): def is_ppc64le() -> bool:
import platform import platform
return platform.machine() == "ppc64le" return platform.machine() == "ppc64le"
def is_win32(): def is_win32() -> bool:
return sys.platform.startswith("win32") return sys.platform.startswith("win32")
def is_pypy(): def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info") return hasattr(sys, "pypy_translation_info")
def is_mingw(): def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw" return sysconfig.get_platform() == "mingw"
class CachedProperty: class CachedProperty:
def __init__(self, func): def __init__(self, func: Callable[[Any], None]) -> None:
self.func = func 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) result = instance.__dict__[self.func.__name__] = self.func(instance)
return result return result

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

View File

@ -1,5 +1,3 @@
#!/usr/bin/gnuplot
#This is the script that was used to create our sample EPS files #This is the script that was used to create our sample EPS files
#We used the following version of the gnuplot program #We used the following version of the gnuplot program
#G N U P L O T #G N U P L O T

BIN
Tests/images/hopper.pfm Normal file

Binary file not shown.

BIN
Tests/images/hopper_be.pfm Normal file

Binary file not shown.

View File

@ -23,7 +23,7 @@ with atheris.instrument_imports():
import fuzzers import fuzzers
def TestOneInput(data): def TestOneInput(data: bytes) -> None:
try: try:
fuzzers.fuzz_font(data) fuzzers.fuzz_font(data)
except Exception: except Exception:
@ -32,7 +32,7 @@ def TestOneInput(data):
pass pass
def main(): def main() -> None:
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput) atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz() atheris.Fuzz()

View File

@ -1,5 +1,3 @@
#!/usr/bin/python3
# Copyright 2020 Google LLC # Copyright 2020 Google LLC
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
@ -23,7 +21,7 @@ with atheris.instrument_imports():
import fuzzers import fuzzers
def TestOneInput(data): def TestOneInput(data: bytes) -> None:
try: try:
fuzzers.fuzz_image(data) fuzzers.fuzz_image(data)
except Exception: except Exception:
@ -32,7 +30,7 @@ def TestOneInput(data):
pass pass
def main(): def main() -> None:
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput) atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz() atheris.Fuzz()

View File

@ -1,22 +1,23 @@
from __future__ import annotations from __future__ import annotations
import io import io
import warnings import warnings
from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont
def enable_decompressionbomb_error(): def enable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
warnings.simplefilter("error", Image.DecompressionBombWarning) warnings.simplefilter("error", Image.DecompressionBombWarning)
def disable_decompressionbomb_error(): def disable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
warnings.resetwarnings() 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 # This will fail on some images in the corpus, as we have many
# invalid images in the test suite. # invalid images in the test suite.
with Image.open(io.BytesIO(data)) as im: with Image.open(io.BytesIO(data)) as im:
@ -25,7 +26,7 @@ def fuzz_image(data):
im.save(io.BytesIO(), "BMP") im.save(io.BytesIO(), "BMP")
def fuzz_font(data): def fuzz_font(data: bytes) -> None:
wrapper = io.BytesIO(data) wrapper = io.BytesIO(data)
try: try:
font = ImageFont.truetype(wrapper) font = ImageFont.truetype(wrapper)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import subprocess import subprocess
import sys import sys
@ -23,7 +24,7 @@ if features.check("libjpeg_turbo"):
"path", "path",
subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), 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() fuzzers.enable_decompressionbomb_error()
try: try:
with open(path, "rb") as f: with open(path, "rb") as f:
@ -54,7 +55,7 @@ def test_fuzz_images(path):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") "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: if not path:
return return
with open(path, "rb") as f: with open(path, "rb") as f:

View File

@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image
def test_sanity(): def test_sanity() -> None:
# Make sure we have the binary extension # Make sure we have the binary extension
Image.core.new("L", (100, 100)) Image.core.new("L", (100, 100))

View File

@ -1,13 +1,14 @@
from __future__ import annotations from __future__ import annotations
from PIL import _binary from PIL import _binary
def test_standard(): def test_standard() -> None:
assert _binary.i8(b"*") == 42 assert _binary.i8(b"*") == 42
assert _binary.o8(42) == b"*" 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.i16le(b"\xff\xff\x00\x00") == 65535
assert _binary.i32le(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" 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.i16be(b"\x00\x00\xff\xff") == 0
assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 assert _binary.i32be(b"\x00\x00\xff\xff") == 65535

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os import os
import warnings import warnings
@ -9,13 +10,13 @@ from .helper import assert_image_similar
base = os.path.join("Tests", "images", "bmp") base = os.path.join("Tests", "images", "bmp")
def get_files(d, ext=".bmp"): def get_files(d: str, ext: str = ".bmp") -> list[str]:
return [ return [
os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f 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 """These shouldn't crash/dos, but they shouldn't return anything
either""" either"""
for f in get_files("b"): for f in get_files("b"):
@ -28,7 +29,7 @@ def test_bad():
pass pass
def test_questionable(): def test_questionable() -> None:
"""These shouldn't crash/dos, but it's not well defined that these """These shouldn't crash/dos, but it's not well defined that these
are in spec""" are in spec"""
supported = [ supported = [
@ -55,7 +56,7 @@ def test_questionable():
raise raise
def test_good(): def test_good() -> None:
"""These should all work. There's a set of target files in the """These should all work. There's a set of target files in the
html directory that we can compare against.""" html directory that we can compare against."""
@ -79,7 +80,7 @@ def test_good():
"rgb32bf.bmp": "rgb24.png", "rgb32bf.bmp": "rgb24.png",
} }
def get_compare(f): def get_compare(f: str) -> str:
name = os.path.split(f)[1] name = os.path.split(f)[1]
if name in file_map: if name in file_map:
return os.path.join(base, "html", file_map[name]) return os.path.join(base, "html", file_map[name])

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, ImageFilter from PIL import Image, ImageFilter
@ -15,18 +16,18 @@ sample.putdata(sum([
# fmt: on # fmt: on
def test_imageops_box_blur(): def test_imageops_box_blur() -> None:
i = sample.filter(ImageFilter.BoxBlur(1)) i = sample.filter(ImageFilter.BoxBlur(1))
assert i.mode == sample.mode assert i.mode == sample.mode
assert i.size == sample.size assert i.size == sample.size
assert isinstance(i, Image.Image) 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)) 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()) it = iter(im.getdata())
for data_row in data: for data_row in data:
im_row = [next(it) for _ in range(im.size[0])] im_row = [next(it) for _ in range(im.size[0])]
@ -36,7 +37,13 @@ def assert_image(im, data, delta=0):
next(it) 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 # check grayscale image
assert_image(box_blur(im, radius, passes), data, delta) assert_image(box_blur(im, radius, passes), data, delta)
rgba = Image.merge("RGBA", (im, im, im, im)) 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) assert_image(band, data, delta)
def test_color_modes(): def test_color_modes() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
box_blur(sample.convert("1")) box_blur(sample.convert("1"))
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -64,7 +71,7 @@ def test_color_modes():
box_blur(sample.convert("YCbCr")) box_blur(sample.convert("YCbCr"))
def test_radius_0(): def test_radius_0() -> None:
assert_blur( assert_blur(
sample, sample,
0, 0,
@ -80,7 +87,7 @@ def test_radius_0():
) )
def test_radius_0_02(): def test_radius_0_02() -> None:
assert_blur( assert_blur(
sample, sample,
0.02, 0.02,
@ -97,7 +104,7 @@ def test_radius_0_02():
) )
def test_radius_0_05(): def test_radius_0_05() -> None:
assert_blur( assert_blur(
sample, sample,
0.05, 0.05,
@ -114,7 +121,7 @@ def test_radius_0_05():
) )
def test_radius_0_1(): def test_radius_0_1() -> None:
assert_blur( assert_blur(
sample, sample,
0.1, 0.1,
@ -131,7 +138,7 @@ def test_radius_0_1():
) )
def test_radius_0_5(): def test_radius_0_5() -> None:
assert_blur( assert_blur(
sample, sample,
0.5, 0.5,
@ -148,7 +155,7 @@ def test_radius_0_5():
) )
def test_radius_1(): def test_radius_1() -> None:
assert_blur( assert_blur(
sample, sample,
1, 1,
@ -165,7 +172,7 @@ def test_radius_1():
) )
def test_radius_1_5(): def test_radius_1_5() -> None:
assert_blur( assert_blur(
sample, sample,
1.5, 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( assert_blur(
sample, sample,
3, 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( assert_blur(
sample, sample,
10, 10,
@ -214,7 +221,7 @@ def test_radius_bigger_then_width():
) )
def test_extreme_large_radius(): def test_extreme_large_radius() -> None:
assert_blur( assert_blur(
sample, sample,
600, 600,
@ -229,7 +236,7 @@ def test_extreme_large_radius():
) )
def test_two_passes(): def test_two_passes() -> None:
assert_blur( assert_blur(
sample, sample,
1, 1,
@ -247,7 +254,7 @@ def test_two_passes():
) )
def test_three_passes(): def test_three_passes() -> None:
assert_blur( assert_blur(
sample, sample,
1, 1,

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from array import array from array import array
from types import ModuleType
import pytest import pytest
@ -7,6 +9,7 @@ from PIL import Image, ImageFilter
from .helper import assert_image_equal from .helper import assert_image_equal
numpy: ModuleType | None
try: try:
import numpy import numpy
except ImportError: except ImportError:
@ -14,7 +17,9 @@ except ImportError:
class TestColorLut3DCoreAPI: 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): if isinstance(size, tuple):
size_1d, size_2d, size_3d = size size_1d, size_2d, size_3d = size
else: else:
@ -40,7 +45,7 @@ class TestColorLut3DCoreAPI:
[item for sublist in table for item in sublist], [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) im = Image.new("RGB", (10, 10), 0)
with pytest.raises(ValueError, match="filter"): with pytest.raises(ValueError, match="filter"):
@ -100,7 +105,7 @@ class TestColorLut3DCoreAPI:
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) 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 = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d( im.im.color_lut_3d(
@ -135,7 +140,7 @@ class TestColorLut3DCoreAPI:
*self.generate_identity_table(3, (3, 3, 65)), *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"): with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0) im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d( im.im.color_lut_3d(
@ -166,7 +171,7 @@ class TestColorLut3DCoreAPI:
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) "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 = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d( im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) "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) "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
) )
def test_identities(self): def test_identities(self) -> None:
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", "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") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", "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") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGBA", "RGBA",
@ -269,7 +274,7 @@ class TestColorLut3DCoreAPI:
), ),
) )
def test_channels_order(self): def test_channels_order(self) -> None:
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", "RGB",
@ -294,7 +299,7 @@ class TestColorLut3DCoreAPI:
]))) ])))
# fmt: on # fmt: on
def test_overflow(self): def test_overflow(self) -> None:
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", "RGB",
@ -347,7 +352,7 @@ class TestColorLut3DCoreAPI:
class TestColorLut3DFilter: class TestColorLut3DFilter:
def test_wrong_args(self): def test_wrong_args(self) -> None:
with pytest.raises(ValueError, match="should be either an integer"): with pytest.raises(ValueError, match="should be either an integer"):
ImageFilter.Color3DLUT("small", [1]) ImageFilter.Color3DLUT("small", [1])
@ -375,7 +380,7 @@ class TestColorLut3DFilter:
with pytest.raises(ValueError, match="Only 3 or 4 output"): with pytest.raises(ValueError, match="Only 3 or 4 output"):
ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) 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) lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
assert tuple(lut.size) == (2, 2, 2) assert tuple(lut.size) == (2, 2, 2)
assert lut.name == "Color 3D LUT" assert lut.name == "Color 3D LUT"
@ -393,7 +398,8 @@ class TestColorLut3DFilter:
assert lut.table == list(range(4)) * 8 assert lut.table == list(range(4)) * 8
@pytest.mark.skipif(numpy is None, reason="NumPy not installed") @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) table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16)
with pytest.raises(ValueError, match="should have either channels"): with pytest.raises(ValueError, match="should have either channels"):
lut = ImageFilter.Color3DLUT((5, 6, 7), table) lut = ImageFilter.Color3DLUT((5, 6, 7), table)
@ -426,7 +432,8 @@ class TestColorLut3DFilter:
assert lut.table[0] == 33 assert lut.table[0] == 33
@pytest.mark.skipif(numpy is None, reason="NumPy not installed") @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") g = Image.linear_gradient("L")
im = Image.merge( im = Image.merge(
"RGB", "RGB",
@ -465,7 +472,7 @@ class TestColorLut3DFilter:
lut.table = numpy.array(lut.table, dtype=numpy.int8) lut.table = numpy.array(lut.table, dtype=numpy.int8)
im.filter(lut) im.filter(lut)
def test_repr(self): def test_repr(self) -> None:
lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
assert repr(lut) == "<Color3DLUT from list size=2x2x2 channels=3>" assert repr(lut) == "<Color3DLUT from list size=2x2x2 channels=3>"
@ -483,7 +490,7 @@ class TestColorLut3DFilter:
class TestGenerateColorLut3D: 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"): with pytest.raises(ValueError, match="3 or 4 output channels"):
ImageFilter.Color3DLUT.generate( ImageFilter.Color3DLUT.generate(
5, channels=2, callback=lambda r, g, b: (r, g, b) 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) 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)) lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
assert tuple(lut.size) == (5, 5, 5) assert tuple(lut.size) == (5, 5, 5)
assert lut.name == "Color 3D LUT" 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] 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 # fmt: on
def test_4_channels(self): def test_4_channels(self) -> None:
lut = ImageFilter.Color3DLUT.generate( lut = ImageFilter.Color3DLUT.generate(
5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2)
) )
@ -520,7 +527,7 @@ class TestGenerateColorLut3D:
] ]
# fmt: on # fmt: on
def test_apply(self): def test_apply(self) -> None:
lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
@ -536,7 +543,7 @@ class TestGenerateColorLut3D:
class TestTransformColorLut3D: 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)) source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
with pytest.raises(ValueError, match="Only 3 or 4 output"): with pytest.raises(ValueError, match="Only 3 or 4 output"):
@ -551,7 +558,7 @@ class TestTransformColorLut3D:
with pytest.raises(TypeError): with pytest.raises(TypeError):
source.transform(lambda r, g, b, a: (r, g, b)) 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( source = ImageFilter.Color3DLUT.generate(
2, lambda r, g, b: (r, g, b), target_mode="HSV" 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") lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB")
assert lut.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)) 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)) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b))
assert tuple(lut.size) == tuple(source.size) assert tuple(lut.size) == tuple(source.size)
@ -570,7 +577,7 @@ class TestTransformColorLut3D:
assert lut.table != source.table 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] 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)) 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) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4)
assert tuple(lut.size) == tuple(source.size) 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] 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]
# fmt: on # fmt: on
def test_4_to_3_channels(self): def test_4_to_3_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate( source = ImageFilter.Color3DLUT.generate(
(3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4 (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] 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]
# fmt: on # fmt: on
def test_4_to_4_channels(self): def test_4_to_4_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate( source = ImageFilter.Color3DLUT.generate(
(6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 (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] 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]
# fmt: on # fmt: on
def test_with_normals_3_channels(self): def test_with_normals_3_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate( source = ImageFilter.Color3DLUT.generate(
(6, 5, 4), lambda r, g, b: (r * r, g * g, b * b) (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] 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]
# fmt: on # fmt: on
def test_with_normals_4_channels(self): def test_with_normals_4_channels(self) -> None:
source = ImageFilter.Color3DLUT.generate( source = ImageFilter.Color3DLUT.generate(
(3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4 (3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4
) )

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
import pytest import pytest
@ -8,7 +9,7 @@ from PIL import Image
from .helper import is_pypy from .helper import is_pypy
def test_get_stats(): def test_get_stats() -> None:
# Create at least one image # Create at least one image
Image.new("RGB", (10, 10)) Image.new("RGB", (10, 10))
@ -21,7 +22,7 @@ def test_get_stats():
assert "blocks_cached" in stats assert "blocks_cached" in stats
def test_reset_stats(): def test_reset_stats() -> None:
Image.core.reset_stats() Image.core.reset_stats()
stats = Image.core.get_stats() stats = Image.core.get_stats()
@ -34,19 +35,19 @@ def test_reset_stats():
class TestCoreMemory: class TestCoreMemory:
def teardown_method(self): def teardown_method(self) -> None:
# Restore default values # Restore default values
Image.core.set_alignment(1) Image.core.set_alignment(1)
Image.core.set_block_size(1024 * 1024) Image.core.set_block_size(1024 * 1024)
Image.core.set_blocks_max(0) Image.core.set_blocks_max(0)
Image.core.clear_cache() Image.core.clear_cache()
def test_get_alignment(self): def test_get_alignment(self) -> None:
alignment = Image.core.get_alignment() alignment = Image.core.get_alignment()
assert alignment > 0 assert alignment > 0
def test_set_alignment(self): def test_set_alignment(self) -> None:
for i in [1, 2, 4, 8, 16, 32]: for i in [1, 2, 4, 8, 16, 32]:
Image.core.set_alignment(i) Image.core.set_alignment(i)
alignment = Image.core.get_alignment() alignment = Image.core.get_alignment()
@ -62,12 +63,12 @@ class TestCoreMemory:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.core.set_alignment(3) 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() block_size = Image.core.get_block_size()
assert block_size >= 4096 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]: for i in [4096, 2 * 4096, 3 * 4096]:
Image.core.set_block_size(i) Image.core.set_block_size(i)
block_size = Image.core.get_block_size() block_size = Image.core.get_block_size()
@ -83,7 +84,7 @@ class TestCoreMemory:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.core.set_block_size(4000) 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.reset_stats()
Image.core.set_blocks_max(0) Image.core.set_blocks_max(0)
Image.core.set_block_size(4096) Image.core.set_block_size(4096)
@ -95,12 +96,12 @@ class TestCoreMemory:
if not is_pypy(): if not is_pypy():
assert stats["freed_blocks"] >= 64 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() blocks_max = Image.core.get_blocks_max()
assert blocks_max >= 0 assert blocks_max >= 0
def test_set_blocks_max(self): def test_set_blocks_max(self) -> None:
for i in [0, 1, 10]: for i in [0, 1, 10]:
Image.core.set_blocks_max(i) Image.core.set_blocks_max(i)
blocks_max = Image.core.get_blocks_max() blocks_max = Image.core.get_blocks_max()
@ -116,7 +117,7 @@ class TestCoreMemory:
Image.core.set_blocks_max(2**29) Image.core.set_blocks_max(2**29)
@pytest.mark.skipif(is_pypy(), reason="Images not collected") @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.reset_stats()
Image.core.set_blocks_max(128) Image.core.set_blocks_max(128)
Image.core.set_block_size(4096) Image.core.set_block_size(4096)
@ -131,7 +132,7 @@ class TestCoreMemory:
assert stats["blocks_cached"] == 64 assert stats["blocks_cached"] == 64
@pytest.mark.skipif(is_pypy(), reason="Images not collected") @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.reset_stats()
Image.core.clear_cache() Image.core.clear_cache()
Image.core.set_blocks_max(128) Image.core.set_blocks_max(128)
@ -148,7 +149,7 @@ class TestCoreMemory:
assert stats["freed_blocks"] >= 48 assert stats["freed_blocks"] >= 48
assert stats["blocks_cached"] == 16 assert stats["blocks_cached"] == 16
def test_large_images(self): def test_large_images(self) -> None:
Image.core.reset_stats() Image.core.reset_stats()
Image.core.set_blocks_max(0) Image.core.set_blocks_max(0)
Image.core.set_block_size(4096) Image.core.set_block_size(4096)
@ -165,14 +166,14 @@ class TestCoreMemory:
class TestEnvVars: class TestEnvVars:
def teardown_method(self): def teardown_method(self) -> None:
# Restore default values # Restore default values
Image.core.set_alignment(1) Image.core.set_alignment(1)
Image.core.set_block_size(1024 * 1024) Image.core.set_block_size(1024 * 1024)
Image.core.set_blocks_max(0) Image.core.set_blocks_max(0)
Image.core.clear_cache() Image.core.clear_cache()
def test_units(self): def test_units(self) -> None:
Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"})
assert Image.core.get_blocks_max() == 2 * 1024 assert Image.core.get_blocks_max() == 2 * 1024
Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"})
@ -186,6 +187,6 @@ class TestEnvVars:
{"PILLOW_BLOCKS_MAX": "wat"}, {"PILLOW_BLOCKS_MAX": "wat"},
), ),
) )
def test_warnings(self, var): def test_warnings(self, var: dict[str, str]) -> None:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
Image._apply_env_variables(var) Image._apply_env_variables(var)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image from PIL import Image
@ -11,16 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb: class TestDecompressionBomb:
def teardown_method(self, method): def teardown_method(self, method) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT 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. # Implicit assert: no warning.
# A warning would cause a failure. # A warning would cause a failure.
with Image.open(TEST_FILE): with Image.open(TEST_FILE):
pass pass
def test_no_warning_no_limit(self): def test_no_warning_no_limit(self) -> None:
# Arrange # Arrange
# Turn limit off # Turn limit off
Image.MAX_IMAGE_PIXELS = None Image.MAX_IMAGE_PIXELS = None
@ -32,7 +33,7 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE): with Image.open(TEST_FILE):
pass pass
def test_warning(self): def test_warning(self) -> None:
# Set limit to trigger warning on the test file # Set limit to trigger warning on the test file
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
assert 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): with Image.open(TEST_FILE):
pass pass
def test_exception(self): def test_exception(self) -> None:
# Set limit to trigger exception on the test file # Set limit to trigger exception on the test file
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
assert 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): with Image.open(TEST_FILE):
pass pass
def test_exception_ico(self): def test_exception_ico(self) -> None:
with pytest.raises(Image.DecompressionBombError): with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/decompression_bomb.ico"): with Image.open("Tests/images/decompression_bomb.ico"):
pass pass
def test_exception_gif(self): def test_exception_gif(self) -> None:
with pytest.raises(Image.DecompressionBombError): with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/decompression_bomb.gif"): with Image.open("Tests/images/decompression_bomb.gif"):
pass 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 Image.open("Tests/images/decompression_bomb_extents.gif") as im:
with pytest.raises(Image.DecompressionBombError): with pytest.raises(Image.DecompressionBombError):
im.seek(1) 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 # Set limit to trigger exception on the test file
Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 Image.MAX_IMAGE_PIXELS = 4 * 64 * 128
assert 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"): with Image.open("Tests/images/zero_width.gif"):
pass pass
def test_exception_bmp(self): def test_exception_bmp(self) -> None:
with pytest.raises(Image.DecompressionBombError): with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/bmp/b/reallybig.bmp"): with Image.open("Tests/images/bmp/b/reallybig.bmp"):
pass pass
@ -82,15 +83,15 @@ class TestDecompressionBomb:
class TestDecompressionCrop: class TestDecompressionCrop:
@classmethod @classmethod
def setup_class(cls): def setup_class(cls) -> None:
width, height = 128, 128 width, height = 128, 128
Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
@classmethod @classmethod
def teardown_class(cls): def teardown_class(cls) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT 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 # Crops can extend the extents, therefore we should have the
# same decompression bomb warnings on them. # same decompression bomb warnings on them.
with hopper() as src: with hopper() as src:
@ -98,7 +99,7 @@ class TestDecompressionCrop:
with pytest.warns(Image.DecompressionBombWarning): with pytest.warns(Image.DecompressionBombWarning):
src.crop(box) src.crop(box)
def test_crop_decompression_checks(self): def test_crop_decompression_checks(self) -> None:
im = Image.new("RGB", (100, 100)) im = Image.new("RGB", (100, 100))
for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)):

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import _deprecate from PIL import _deprecate
@ -19,12 +20,12 @@ from PIL import _deprecate
), ),
], ],
) )
def test_version(version, expected): def test_version(version, expected) -> None:
with pytest.warns(DeprecationWarning, match=expected): with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", version, "new thing") _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\?" expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?"
with pytest.raises(ValueError, match=expected): with pytest.raises(ValueError, match=expected):
_deprecate.deprecate("Old thing", 12345, "new thing") _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, plural, expected) -> None:
expected = r"" expected = r""
with pytest.raises(RuntimeError, match=expected): with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural) _deprecate.deprecate(deprecated, 1, plural=plural)
def test_plural(): def test_plural() -> None:
expected = ( expected = (
r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Use new thing instead\." r"Use new thing instead\."
@ -60,7 +61,7 @@ def test_plural():
_deprecate.deprecate("Old things", 11, "new thing", plural=True) _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'" expected = "Use only one of 'replacement' and 'action'"
with pytest.raises(ValueError, match=expected): with pytest.raises(ValueError, match=expected):
_deprecate.deprecate( _deprecate.deprecate(
@ -75,7 +76,7 @@ def test_replacement_and_action():
"Upgrade to new thing.", "Upgrade to new thing.",
], ],
) )
def test_action(action): def test_action(action) -> None:
expected = ( expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Upgrade to new thing\." r"Upgrade to new thing\."
@ -84,7 +85,7 @@ def test_action(action):
_deprecate.deprecate("Old thing", 11, action=action) _deprecate.deprecate("Old thing", 11, action=action)
def test_no_replacement_or_action(): def test_no_replacement_or_action() -> None:
expected = ( expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)"
) )

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import re import re
@ -14,7 +15,7 @@ except ImportError:
pass pass
def test_check(): def test_check() -> None:
# Check the correctness of the convenience function # Check the correctness of the convenience function
for module in features.modules: for module in features.modules:
assert features.check_module(module) == features.check(module) assert features.check_module(module) == features.check(module)
@ -24,11 +25,11 @@ def test_check():
assert features.check_feature(feature) == features.check(feature) assert features.check_feature(feature) == features.check(feature)
def test_version(): def test_version() -> None:
# Check the correctness of the convenience function # Check the correctness of the convenience function
# and the format of version numbers # and the format of version numbers
def test(name, function): def test(name, function) -> None:
version = features.version(name) version = features.version(name)
if not features.check(name): if not features.check(name):
assert version is None assert version is None
@ -46,56 +47,56 @@ def test_version():
@skip_unless_feature("webp") @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.WebPDecoderBuggyAlpha()
assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY
@skip_unless_feature("webp") @skip_unless_feature("webp")
def test_webp_mux(): def test_webp_mux() -> None:
assert features.check("webp_mux") == _webp.HAVE_WEBPMUX assert features.check("webp_mux") == _webp.HAVE_WEBPMUX
@skip_unless_feature("webp") @skip_unless_feature("webp")
def test_webp_anim(): def test_webp_anim() -> None:
assert features.check("webp_anim") == _webp.HAVE_WEBPANIM assert features.check("webp_anim") == _webp.HAVE_WEBPANIM
@skip_unless_feature("libjpeg_turbo") @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")) assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo"))
@skip_unless_feature("libimagequant") @skip_unless_feature("libimagequant")
def test_libimagequant_version(): def test_libimagequant_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
@pytest.mark.parametrize("feature", features.modules) @pytest.mark.parametrize("feature", features.modules)
def test_check_modules(feature): def test_check_modules(feature) -> None:
assert features.check_module(feature) in [True, False] assert features.check_module(feature) in [True, False]
@pytest.mark.parametrize("feature", features.codecs) @pytest.mark.parametrize("feature", features.codecs)
def test_check_codecs(feature): def test_check_codecs(feature) -> None:
assert features.check_codec(feature) in [True, False] 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: with pytest.warns(UserWarning) as cm:
has_feature = features.check("typo") has_feature = features.check("typo")
assert has_feature is False assert has_feature is False
assert str(cm[-1].message) == "Unknown feature 'typo'." 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_modules(), list)
assert isinstance(features.get_supported_codecs(), list) assert isinstance(features.get_supported_codecs(), list)
assert isinstance(features.get_supported_features(), list) assert isinstance(features.get_supported_features(), list)
assert isinstance(features.get_supported(), list) assert isinstance(features.get_supported(), list)
def test_unsupported_codec(): def test_unsupported_codec() -> None:
# Arrange # Arrange
codec = "unsupported_codec" codec = "unsupported_codec"
# Act / Assert # Act / Assert
@ -105,7 +106,7 @@ def test_unsupported_codec():
features.version_codec(codec) features.version_codec(codec)
def test_unsupported_module(): def test_unsupported_module() -> None:
# Arrange # Arrange
module = "unsupported_module" module = "unsupported_module"
# Act / Assert # Act / Assert
@ -115,7 +116,7 @@ def test_unsupported_module():
features.version_module(module) features.version_module(module)
def test_pilinfo(): def test_pilinfo() -> None:
buf = io.StringIO() buf = io.StringIO()
features.pilinfo(buf) features.pilinfo(buf)
out = buf.getvalue() out = buf.getvalue()

View File

@ -1,4 +1,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import Image, ImageSequence, PngImagePlugin from PIL import Image, ImageSequence, PngImagePlugin
@ -7,7 +10,7 @@ from PIL import Image, ImageSequence, PngImagePlugin
# APNG browser support tests and fixtures via: # APNG browser support tests and fixtures via:
# https://philip.html5.org/tests/apng/tests.html # https://philip.html5.org/tests/apng/tests.html
# (referenced from https://wiki.mozilla.org/APNG_Specification) # (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: with Image.open("Tests/images/apng/single_frame.png") as im:
assert not im.is_animated assert not im.is_animated
assert im.n_frames == 1 assert im.n_frames == 1
@ -44,14 +47,14 @@ def test_apng_basic():
"filename", "filename",
("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), ("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: with Image.open(filename) as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
def test_apng_dispose(): def test_apng_dispose() -> None:
with Image.open("Tests/images/apng/dispose_op_none.png") as im: with Image.open("Tests/images/apng/dispose_op_none.png") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
@ -83,7 +86,7 @@ def test_apng_dispose():
assert im.getpixel((64, 32)) == (0, 0, 0, 0) 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: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
@ -105,7 +108,7 @@ def test_apng_dispose_region():
assert im.getpixel((64, 32)) == (0, 255, 0, 255) 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 # Test that the dispose settings being used are from the previous frame
# #
# Image created with: # Image created with:
@ -130,14 +133,14 @@ def test_apng_dispose_op_previous_frame():
assert im.getpixel((0, 0)) == (255, 0, 0, 255) 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: with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im:
im.seek(1) im.seek(1)
im.load() im.load()
assert im.size == (128, 64) 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: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
@ -164,20 +167,20 @@ def test_apng_blend():
assert im.getpixel((64, 32)) == (0, 255, 0, 255) 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: with Image.open("Tests/images/blend_transparency.png") as im:
im.seek(1) im.seek(1)
assert im.getpixel((0, 0)) == (255, 0, 0) 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: with Image.open("Tests/images/apng/fctl_actl.png") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
def test_apng_delay(): def test_apng_delay() -> None:
with Image.open("Tests/images/apng/delay.png") as im: with Image.open("Tests/images/apng/delay.png") as im:
im.seek(1) im.seek(1)
assert im.info.get("duration") == 500.0 assert im.info.get("duration") == 500.0
@ -217,7 +220,7 @@ def test_apng_delay():
assert im.info.get("duration") == 1000.0 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: with Image.open("Tests/images/apng/num_plays.png") as im:
assert im.info.get("loop") == 0 assert im.info.get("loop") == 0
@ -225,7 +228,7 @@ def test_apng_num_plays():
assert im.info.get("loop") == 1 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: with Image.open("Tests/images/apng/mode_16bit.png") as im:
assert im.mode == "RGBA" assert im.mode == "RGBA"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
@ -266,7 +269,7 @@ def test_apng_mode():
assert im.getpixel((64, 32)) == (0, 0, 255, 128) 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: with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
assert not im.is_animated assert not im.is_animated
@ -291,7 +294,7 @@ def test_apng_chunk_errors():
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
def test_apng_syntax_errors(): def test_apng_syntax_errors() -> None:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
assert not im.is_animated assert not im.is_animated
@ -335,14 +338,14 @@ def test_apng_syntax_errors():
"sequence_fdat_fctl.png", "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 pytest.raises(SyntaxError):
with Image.open(f"Tests/images/apng/{test_file}") as im: with Image.open(f"Tests/images/apng/{test_file}") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() 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: with Image.open("Tests/images/apng/single_frame.png") as im:
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
im.save(test_file, save_all=True) 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) 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") test_file = str(tmp_path / "temp.png")
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) 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) 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 # test to make sure we do not generate sequence errors when writing
# frames with image data spanning multiple fdAT chunks (in this case # frames with image data spanning multiple fdAT chunks (in this case
# both the default image and first animation frame will span multiple # both the default image and first animation frame will span multiple
@ -411,7 +414,7 @@ def test_apng_save_split_fdat(tmp_path):
assert exception is None 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") test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/apng/delay.png") as im: with Image.open("Tests/images/apng/delay.png") as im:
frames = [] frames = []
@ -474,7 +477,7 @@ def test_apng_save_duration_loop(tmp_path):
assert im.info["duration"] == 600 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") test_file = str(tmp_path / "temp.png")
size = (128, 64) size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255))
@ -575,7 +578,7 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 0, 0, 0) 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") test_file = str(tmp_path / "temp.png")
size = (128, 64) size = (128, 64)
blue = Image.new("RGBA", size, (0, 0, 255, 255)) blue = Image.new("RGBA", size, (0, 0, 255, 255))
@ -597,7 +600,7 @@ def test_apng_save_disposal_previous(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255) 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") test_file = str(tmp_path / "temp.png")
size = (128, 64) size = (128, 64)
red = Image.new("RGBA", size, (255, 0, 0, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255))
@ -665,7 +668,7 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((0, 0)) == (0, 255, 0, 255) 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 = Image.open("Tests/images/apng/delay.png")
im.seek(1) im.seek(1)
im.close() im.close()
@ -677,7 +680,9 @@ def test_seek_after_close():
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) @pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
@pytest.mark.parametrize("default_image", (True, False)) @pytest.mark.parametrize("default_image", (True, False))
@pytest.mark.parametrize("duplicate", (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") test_file = str(tmp_path / "temp.png")
im = Image.new("L", (1, 1)) 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: with Image.open(test_file) as reloaded:
assert reloaded.mode == mode 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

View File

@ -1,4 +1,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import Image 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: with Image.open("Tests/images/blp/blp1_jpeg.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
@ -19,22 +22,22 @@ def test_load_blp1():
im.load() im.load()
def test_load_blp2_raw(): def test_load_blp2_raw() -> None:
with Image.open("Tests/images/blp/blp2_raw.blp") as im: with Image.open("Tests/images/blp/blp2_raw.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png") 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: with Image.open("Tests/images/blp/blp2_dxt1.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png") 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: with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") 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") f = str(tmp_path / "temp.blp")
for version in ("BLP1", "BLP2"): for version in ("BLP1", "BLP2"):
@ -68,7 +71,7 @@ def test_save(tmp_path):
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
], ],
) )
def test_crashes(test_file): def test_crashes(test_file) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
with Image.open(f) as im: with Image.open(f) as im:
with pytest.raises(OSError): with pytest.raises(OSError):

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import io import io
from pathlib import Path
import pytest import pytest
@ -13,8 +15,8 @@ from .helper import (
) )
def test_sanity(tmp_path): def test_sanity(tmp_path: Path) -> None:
def roundtrip(im): def roundtrip(im) -> None:
outfile = str(tmp_path / "temp.bmp") outfile = str(tmp_path / "temp.bmp")
im.save(outfile, "BMP") im.save(outfile, "BMP")
@ -34,20 +36,20 @@ def test_sanity(tmp_path):
roundtrip(hopper("RGB")) roundtrip(hopper("RGB"))
def test_invalid_file(): def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp: with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
BmpImagePlugin.BmpImageFile(fp) BmpImagePlugin.BmpImageFile(fp)
def test_fallback_if_mmap_errors(): def test_fallback_if_mmap_errors() -> None:
# This image has been truncated, # This image has been truncated,
# so that the buffer is not large enough when using mmap # so that the buffer is not large enough when using mmap
with Image.open("Tests/images/mmap_error.bmp") as im: with Image.open("Tests/images/mmap_error.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp")
def test_save_to_bytes(): def test_save_to_bytes() -> None:
output = io.BytesIO() output = io.BytesIO()
im = hopper() im = hopper()
im.save(output, "BMP") im.save(output, "BMP")
@ -59,7 +61,7 @@ def test_save_to_bytes():
assert reloaded.format == "BMP" assert reloaded.format == "BMP"
def test_small_palette(tmp_path): def test_small_palette(tmp_path: Path) -> None:
im = Image.new("P", (1, 1)) im = Image.new("P", (1, 1))
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
im.putpalette(colors) im.putpalette(colors)
@ -71,7 +73,7 @@ def test_small_palette(tmp_path):
assert reloaded.getpalette() == colors 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") outfile = str(tmp_path / "temp.bmp")
with Image.new("RGB", (1, 1)) as im: with Image.new("RGB", (1, 1)) as im:
im._size = (37838, 37838) im._size = (37838, 37838)
@ -79,7 +81,7 @@ def test_save_too_large(tmp_path):
im.save(outfile) im.save(outfile)
def test_dpi(): def test_dpi() -> None:
dpi = (72, 72) dpi = (72, 72)
output = io.BytesIO() output = io.BytesIO()
@ -91,7 +93,7 @@ def test_dpi():
assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) 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 # Test for #1301
# Arrange # Arrange
outfile = str(tmp_path / "temp.jpg") outfile = str(tmp_path / "temp.jpg")
@ -109,7 +111,7 @@ def test_save_bmp_with_dpi(tmp_path):
assert reloaded.format == "JPEG" 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") outfile = str(tmp_path / "temp.bmp")
with Image.open("Tests/images/hopper.bmp") as im: with Image.open("Tests/images/hopper.bmp") as im:
im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
@ -117,7 +119,7 @@ def test_save_float_dpi(tmp_path):
assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) 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 # test for #1293, Imagegrab returning Unsupported Bitfields Format
with Image.open("Tests/images/clipboard.dib") as im: with Image.open("Tests/images/clipboard.dib") as im:
assert im.format == "DIB" assert im.format == "DIB"
@ -126,7 +128,7 @@ def test_load_dib():
assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") 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") outfile = str(tmp_path / "temp.dib")
with Image.open("Tests/images/clipboard.dib") as im: with Image.open("Tests/images/clipboard.dib") as im:
@ -138,7 +140,7 @@ def test_save_dib(tmp_path):
assert_image_equal(im, reloaded) assert_image_equal(im, reloaded)
def test_rgba_bitfields(): def test_rgba_bitfields() -> None:
# This test image has been manually hexedited # This test image has been manually hexedited
# to change the bitfield compression in the header from XBGR to RGBA # to change the bitfield compression in the header from XBGR to RGBA
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: 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: with Image.open("Tests/images/hopper_rle8.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
@ -176,7 +178,7 @@ def test_rle8():
im.load() im.load()
def test_rle4(): def test_rle4() -> None:
with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im: with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im:
assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12) 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), ("Tests/images/bmp/g/pal8rle.bmp", 1064),
), ),
) )
def test_rle8_eof(file_name, length): def test_rle8_eof(file_name, length) -> None:
with open(file_name, "rb") as fp: with open(file_name, "rb") as fp:
data = fp.read(length) data = fp.read(length)
with Image.open(io.BytesIO(data)) as im: with Image.open(io.BytesIO(data)) as im:
@ -200,7 +202,7 @@ def test_rle8_eof(file_name, length):
im.load() im.load()
def test_offset(): def test_offset() -> None:
# This image has been hexedited # This image has been hexedited
# to exclude the palette size from the pixel data offset # to exclude the palette size from the pixel data offset
with Image.open("Tests/images/pal8_offset.bmp") as im: with Image.open("Tests/images/pal8_offset.bmp") as im:

View File

@ -1,4 +1,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import BufrStubImagePlugin, Image from PIL import BufrStubImagePlugin, Image
@ -8,7 +11,7 @@ from .helper import hopper
TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d"
def test_open(): def test_open() -> None:
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
@ -19,7 +22,7 @@ def test_open():
assert im.size == (1, 1) assert im.size == (1, 1)
def test_invalid_file(): def test_invalid_file() -> None:
# Arrange # Arrange
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
@ -28,7 +31,7 @@ def test_invalid_file():
BufrStubImagePlugin.BufrStubImageFile(invalid_file) BufrStubImagePlugin.BufrStubImageFile(invalid_file)
def test_load(): def test_load() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler # Act / Assert: stub cannot load without an implemented handler
@ -36,7 +39,7 @@ def test_load():
im.load() im.load()
def test_save(tmp_path): def test_save(tmp_path: Path) -> None:
# Arrange # Arrange
im = hopper() im = hopper()
tmpfile = str(tmp_path / "temp.bufr") tmpfile = str(tmp_path / "temp.bufr")
@ -46,13 +49,13 @@ def test_save(tmp_path):
im.save(tmpfile) im.save(tmpfile)
def test_handler(tmp_path): def test_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler:
opened = False opened = False
loaded = False loaded = False
saved = False saved = False
def open(self, im): def open(self, im) -> None:
self.opened = True self.opened = True
def load(self, im): def load(self, im):
@ -60,7 +63,7 @@ def test_handler(tmp_path):
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) return Image.new("RGB", (1, 1))
def save(self, im, fp, filename): def save(self, im, fp, filename) -> None:
self.saved = True self.saved = True
handler = TestHandler() handler = TestHandler()

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import ContainerIO, Image from PIL import ContainerIO, Image
@ -8,19 +9,19 @@ from .helper import hopper
TEST_FILE = "Tests/images/dummy.container" TEST_FILE = "Tests/images/dummy.container"
def test_sanity(): def test_sanity() -> None:
dir(Image) dir(Image)
dir(ContainerIO) dir(ContainerIO)
def test_isatty(): def test_isatty() -> None:
with hopper() as im: with hopper() as im:
container = ContainerIO.ContainerIO(im, 0, 0) container = ContainerIO.ContainerIO(im, 0, 0)
assert container.isatty() is False assert container.isatty() is False
def test_seek_mode_0(): def test_seek_mode_0() -> None:
# Arrange # Arrange
mode = 0 mode = 0
with open(TEST_FILE, "rb") as fh: with open(TEST_FILE, "rb") as fh:
@ -34,7 +35,7 @@ def test_seek_mode_0():
assert container.tell() == 33 assert container.tell() == 33
def test_seek_mode_1(): def test_seek_mode_1() -> None:
# Arrange # Arrange
mode = 1 mode = 1
with open(TEST_FILE, "rb") as fh: with open(TEST_FILE, "rb") as fh:
@ -48,7 +49,7 @@ def test_seek_mode_1():
assert container.tell() == 66 assert container.tell() == 66
def test_seek_mode_2(): def test_seek_mode_2() -> None:
# Arrange # Arrange
mode = 2 mode = 2
with open(TEST_FILE, "rb") as fh: with open(TEST_FILE, "rb") as fh:
@ -63,7 +64,7 @@ def test_seek_mode_2():
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n0(bytesmode): def test_read_n0(bytesmode: bool) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -79,7 +80,7 @@ def test_read_n0(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n(bytesmode): def test_read_n(bytesmode: bool) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -95,7 +96,7 @@ def test_read_n(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_eof(bytesmode): def test_read_eof(bytesmode: bool) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -111,7 +112,7 @@ def test_read_eof(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_readline(bytesmode): def test_readline(bytesmode: bool) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120) container = ContainerIO.ContainerIO(fh, 0, 120)
@ -126,7 +127,7 @@ def test_readline(bytesmode):
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_readlines(bytesmode): def test_readlines(bytesmode: bool) -> None:
# Arrange # Arrange
expected = [ expected = [
"This is line 1\n", "This is line 1\n",

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import CurImagePlugin, Image from PIL import CurImagePlugin, Image
@ -6,7 +7,7 @@ from PIL import CurImagePlugin, Image
TEST_FILE = "Tests/images/deerstalker.cur" TEST_FILE = "Tests/images/deerstalker.cur"
def test_sanity(): def test_sanity() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.size == (32, 32) assert im.size == (32, 32)
assert isinstance(im, CurImagePlugin.CurImageFile) assert isinstance(im, CurImagePlugin.CurImageFile)
@ -16,7 +17,7 @@ def test_sanity():
assert im.getpixel((16, 16)) == (84, 87, 86, 255) assert im.getpixel((16, 16)) == (84, 87, 86, 255)
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
import pytest import pytest
@ -11,7 +12,7 @@ from .helper import assert_image_equal, hopper, is_pypy
TEST_FILE = "Tests/images/hopper.dcx" TEST_FILE = "Tests/images/hopper.dcx"
def test_sanity(): def test_sanity() -> None:
# Arrange # Arrange
# Act # Act
@ -24,8 +25,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(): def test_unclosed_file() -> None:
def open(): def open() -> None:
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
@ -33,26 +34,26 @@ def test_unclosed_file():
open() open()
def test_closed_file(): def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
im.close() im.close()
def test_context_manager(): def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()
def test_invalid_file(): def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp: with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
DcxImagePlugin.DcxImageFile(fp) DcxImagePlugin.DcxImageFile(fp)
def test_tell(): def test_tell() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act # Act
@ -62,13 +63,13 @@ def test_tell():
assert frame == 0 assert frame == 0
def test_n_frames(): def test_n_frames() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
def test_eoferror(): def test_eoferror() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
n_frames = im.n_frames n_frames = im.n_frames
@ -81,7 +82,7 @@ def test_eoferror():
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_seek_too_far(): def test_seek_too_far() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
frame = 999 # too big on purpose frame = 999 # too big on purpose

View File

@ -1,6 +1,9 @@
"""Test DdsImagePlugin""" """Test DdsImagePlugin"""
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
from pathlib import Path
import pytest import pytest
@ -45,7 +48,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
TEST_FILE_DX10_BC1_TYPELESS, 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""" """Check DXT1 and BC1 images can be opened"""
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
target = target.convert("RGBA") target = target.convert("RGBA")
@ -59,7 +62,7 @@ def test_sanity_dxt1_bc1(image_path):
assert_image_equal(im, target) assert_image_equal(im, target)
def test_sanity_dxt3(): def test_sanity_dxt3() -> None:
"""Check DXT3 images can be opened""" """Check DXT3 images can be opened"""
with Image.open(TEST_FILE_DXT3) as im: with Image.open(TEST_FILE_DXT3) as im:
@ -72,7 +75,7 @@ def test_sanity_dxt3():
assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) 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""" """Check DXT5 images can be opened"""
with Image.open(TEST_FILE_DXT5) as im: with Image.open(TEST_FILE_DXT5) as im:
@ -93,7 +96,7 @@ def test_sanity_dxt5():
TEST_FILE_BC4U, 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""" """Check ATI1 and BC4U images can be opened"""
with Image.open(image_path) as im: with Image.open(image_path) as im:
@ -114,7 +117,7 @@ def test_sanity_ati1_bc4u(image_path):
TEST_FILE_DX10_BC4_TYPELESS, 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""" """Check DX10 BC4 images can be opened"""
with Image.open(image_path) as im: with Image.open(image_path) as im:
@ -135,7 +138,7 @@ def test_dx10_bc4(image_path):
TEST_FILE_BC5U, 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""" """Check ATI2 and BC5U images can be opened"""
with Image.open(image_path) as im: with Image.open(image_path) as im:
@ -159,7 +162,7 @@ def test_sanity_ati2_bc5u(image_path):
(TEST_FILE_BC5S, TEST_FILE_BC5S), (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""" """Check DX10 BC5 images can be opened"""
with Image.open(image_path) as im: with Image.open(image_path) as im:
@ -173,7 +176,7 @@ def test_dx10_bc5(image_path, expected_path):
@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) @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""" """Check DX10 BC6H/BC6HS images can be opened"""
with Image.open(image_path) as im: with Image.open(image_path) as im:
@ -186,7 +189,7 @@ def test_dx10_bc6h(image_path):
assert_image_equal_tofile(im, image_path.replace(".dds", ".png")) 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""" """Check DX10 images can be opened"""
with Image.open(TEST_FILE_DX10_BC7) as im: with Image.open(TEST_FILE_DX10_BC7) as im:
@ -199,7 +202,7 @@ def test_dx10_bc7():
assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png")) 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""" """Check DX10 unsigned normalized integer images can be opened"""
with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im:
@ -215,7 +218,7 @@ def test_dx10_bc7_unorm_srgb():
) )
def test_dx10_r8g8b8a8(): def test_dx10_r8g8b8a8() -> None:
"""Check DX10 images can be opened""" """Check DX10 images can be opened"""
with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: with Image.open(TEST_FILE_DX10_R8G8B8A8) as im:
@ -228,7 +231,7 @@ def test_dx10_r8g8b8a8():
assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) 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""" """Check DX10 unsigned normalized integer images can be opened"""
with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im: with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im:
@ -254,7 +257,7 @@ def test_dx10_r8g8b8a8_unorm_srgb():
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), ("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""" """Check uncompressed images can be opened"""
with Image.open(test_file) as im: with Image.open(test_file) as im:
@ -265,7 +268,7 @@ def test_uncompressed(mode, size, test_file):
assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) assert_image_equal_tofile(im, test_file.replace(".dds", ".png"))
def test__accept_true(): def test__accept_true() -> None:
"""Check valid prefix""" """Check valid prefix"""
# Arrange # Arrange
prefix = b"DDS etc" prefix = b"DDS etc"
@ -277,7 +280,7 @@ def test__accept_true():
assert output assert output
def test__accept_false(): def test__accept_false() -> None:
"""Check invalid prefix""" """Check invalid prefix"""
# Arrange # Arrange
prefix = b"something invalid" prefix = b"something invalid"
@ -289,19 +292,19 @@ def test__accept_false():
assert not output assert not output
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
DdsImagePlugin.DdsImageFile(invalid_file) DdsImagePlugin.DdsImageFile(invalid_file)
def test_short_header(): def test_short_header() -> None:
"""Check a short header""" """Check a short header"""
with open(TEST_FILE_DXT5, "rb") as f: with open(TEST_FILE_DXT5, "rb") as f:
img_file = f.read() img_file = f.read()
def short_header(): def short_header() -> None:
with Image.open(BytesIO(img_file[:119])): with Image.open(BytesIO(img_file[:119])):
pass # pragma: no cover pass # pragma: no cover
@ -309,13 +312,13 @@ def test_short_header():
short_header() short_header()
def test_short_file(): def test_short_file() -> None:
"""Check that the appropriate error is thrown for a short file""" """Check that the appropriate error is thrown for a short file"""
with open(TEST_FILE_DXT5, "rb") as f: with open(TEST_FILE_DXT5, "rb") as f:
img_file = f.read() img_file = f.read()
def short_file(): def short_file() -> None:
with Image.open(BytesIO(img_file[:-100])) as im: with Image.open(BytesIO(img_file[:-100])) as im:
im.load() im.load()
@ -323,7 +326,7 @@ def test_short_file():
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""" """Check that colorblocks are decoded correctly in DXT5"""
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
@ -338,12 +341,12 @@ def test_dxt5_colorblock_alpha_issue_4142():
assert px[2] != 0 assert px[2] != 0
def test_palette(): def test_palette() -> None:
with Image.open("Tests/images/palette.dds") as im: with Image.open("Tests/images/palette.dds") as im:
assert_image_equal_tofile(im, "Tests/images/transparent.gif") assert_image_equal_tofile(im, "Tests/images/transparent.gif")
def test_unsupported_bitcount(): def test_unsupported_bitcount() -> None:
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open("Tests/images/unsupported_bitcount.dds"): with Image.open("Tests/images/unsupported_bitcount.dds"):
pass pass
@ -356,13 +359,13 @@ def test_unsupported_bitcount():
"Tests/images/unimplemented_pfflags.dds", "Tests/images/unimplemented_pfflags.dds",
), ),
) )
def test_not_implemented(test_file): def test_not_implemented(test_file: str) -> None:
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
with Image.open(test_file): with Image.open(test_file):
pass pass
def test_save_unsupported_mode(tmp_path): def test_save_unsupported_mode(tmp_path: Path) -> None:
out = str(tmp_path / "temp.dds") out = str(tmp_path / "temp.dds")
im = hopper("HSV") im = hopper("HSV")
with pytest.raises(OSError): with pytest.raises(OSError):
@ -378,7 +381,7 @@ def test_save_unsupported_mode(tmp_path):
("RGBA", "Tests/images/pil123rgba.png"), ("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") out = str(tmp_path / "temp.dds")
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.mode == mode assert im.mode == mode

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import io import io
from pathlib import Path
import pytest import pytest
@ -82,7 +84,7 @@ simple_eps_file_with_long_binary_data = (
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
) )
@pytest.mark.parametrize("scale", (1, 2)) @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) expected_size = tuple(s * scale for s in size)
with Image.open(filename) as image: with Image.open(filename) as image:
image.load(scale=scale) image.load(scale=scale)
@ -92,7 +94,7 @@ def test_sanity(filename, size, scale):
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_load(): def test_load() -> None:
with Image.open(FILE1) as im: with Image.open(FILE1) as im:
assert im.load()[0, 0] == (255, 255, 255) assert im.load()[0, 0] == (255, 255, 255)
@ -100,7 +102,7 @@ def test_load():
assert im.load()[0, 0] == (255, 255, 255) assert im.load()[0, 0] == (255, 255, 255)
def test_binary(): def test_binary() -> None:
if HAS_GHOSTSCRIPT: if HAS_GHOSTSCRIPT:
assert EpsImagePlugin.gs_binary is not None assert EpsImagePlugin.gs_binary is not None
else: else:
@ -114,41 +116,41 @@ def test_binary():
assert EpsImagePlugin.gs_windows_binary is not None assert EpsImagePlugin.gs_windows_binary is not None
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
EpsImagePlugin.EpsImageFile(invalid_file) EpsImagePlugin.EpsImageFile(invalid_file)
def test_binary_header_only(): def test_binary_header_only() -> None:
data = io.BytesIO(simple_binary_header) data = io.BytesIO(simple_binary_header)
with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @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)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @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)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox))
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @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)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
with pytest.raises(OSError, match="cannot determine EPS bounding box"): with pytest.raises(OSError, match="cannot determine EPS bounding box"):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @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( data = io.BytesIO(
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) 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)) @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)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
with pytest.raises(SyntaxError, match="not an EPS file"): with pytest.raises(SyntaxError, match="not an EPS file"):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @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)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @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)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
with Image.open(data) as img: with Image.open(data) as img:
img.load() 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.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @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: with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
assert cmyk_image.mode == "CMYK" assert cmyk_image.mode == "CMYK"
assert cmyk_image.size == (100, 100) assert cmyk_image.size == (100, 100)
@ -202,7 +204,7 @@ def test_cmyk():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @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 # 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.eps") as plot_image:
with Image.open("Tests/images/reqd_showpage.png") as target: 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") @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: with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
plot_image.load(transparency=True) plot_image.load(transparency=True)
assert plot_image.mode == "RGBA" assert plot_image.mode == "RGBA"
@ -224,7 +226,7 @@ def test_transparency():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @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 # issue 479
with Image.open(FILE1) as image1: with Image.open(FILE1) as image1:
with open(str(tmp_path / "temp.eps"), "wb") as fh: with open(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") @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: with open(FILE1, "rb") as f:
img_bytes = io.BytesIO(f.read()) img_bytes = io.BytesIO(f.read())
@ -245,12 +247,12 @@ def test_bytesio_object():
assert_image_similar(img, image1_scale1_compare, 5) 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: with Image.open("Tests/images/1.eps") as im:
assert im.mode == "1" 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") im = hopper("RGBA")
tmpfile = str(tmp_path / "temp.eps") tmpfile = str(tmp_path / "temp.eps")
with pytest.raises(ValueError): 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") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@skip_unless_feature("zlib") @skip_unless_feature("zlib")
def test_render_scale1(): def test_render_scale1() -> None:
# We need png support for these render test # We need png support for these render test
# Zero bounding box # Zero bounding box
@ -270,7 +272,7 @@ def test_render_scale1():
image1_scale1_compare.load() image1_scale1_compare.load()
assert_image_similar(image1_scale1, image1_scale1_compare, 5) assert_image_similar(image1_scale1, image1_scale1_compare, 5)
# Non-Zero bounding box # Non-zero bounding box
with Image.open(FILE2) as image2_scale1: with Image.open(FILE2) as image2_scale1:
image2_scale1.load() image2_scale1.load()
with Image.open(FILE2_COMPARE) as image2_scale1_compare: 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") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@skip_unless_feature("zlib") @skip_unless_feature("zlib")
def test_render_scale2(): def test_render_scale2() -> None:
# We need png support for these render test # We need png support for these render test
# Zero bounding box # Zero bounding box
@ -292,7 +294,7 @@ def test_render_scale2():
image1_scale2_compare.load() image1_scale2_compare.load()
assert_image_similar(image1_scale2, image1_scale2_compare, 5) assert_image_similar(image1_scale2, image1_scale2_compare, 5)
# Non-Zero bounding box # Non-zero bounding box
with Image.open(FILE2) as image2_scale2: with Image.open(FILE2) as image2_scale2:
image2_scale2.load(scale=2) image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
@ -303,7 +305,7 @@ def test_render_scale2():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) @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: with Image.open(filename) as im:
new_size = (100, 100) new_size = (100, 100)
im = im.resize(new_size) 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.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("filename", (FILE1, FILE2)) @pytest.mark.parametrize("filename", (FILE1, FILE2))
def test_thumbnail(filename): def test_thumbnail(filename: str) -> None:
# Issue #619 # Issue #619
with Image.open(filename) as im: with Image.open(filename) as im:
new_size = (100, 100) new_size = (100, 100)
@ -320,20 +322,20 @@ def test_thumbnail(filename):
assert max(im.size) == max(new_size) assert max(im.size) == max(new_size)
def test_read_binary_preview(): def test_read_binary_preview() -> None:
# Issue 302 # Issue 302
# open image with binary preview # open image with binary preview
with Image.open(FILE3): with Image.open(FILE3):
pass 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 # check all the freaking line endings possible from the spec
# test_string = u'something\r\nelse\n\rbaz\rbif\n' # test_string = u'something\r\nelse\n\rbaz\rbif\n'
line_endings = ["\r\n", "\n", "\n\r", "\r"] line_endings = ["\r\n", "\n", "\n\r", "\r"]
strings = ["something", "else", "baz", "bif"] 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" % ( ending = "Failure with line ending: %s" % (
"".join("%s" % ord(s) for s in ending) "".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") == "baz", ending
assert t.readline().strip("\r\n") == "bif", 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")) f = io.BytesIO(test_string.encode("latin-1"))
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(f) t = EpsImagePlugin.PSFile(f)
_test_readline(t, ending) _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") f = str(tmp_path / "temp.txt")
with open(f, "wb") as w: with open(f, "wb") as w:
w.write(test_string.encode("latin-1")) w.write(test_string.encode("latin-1"))
@ -364,7 +366,7 @@ def test_readline_psfile(tmp_path):
_test_readline_file_psfile(s, ending) _test_readline_file_psfile(s, ending)
def test_psfile_deprecation(): def test_psfile_deprecation() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
EpsImagePlugin.PSFile(None) EpsImagePlugin.PSFile(None)
@ -374,7 +376,7 @@ def test_psfile_deprecation():
"line_ending", "line_ending",
(b"\r\n", b"\n", b"\n\r", b"\r"), (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) simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
data = io.BytesIO(simple_file) data = io.BytesIO(simple_file)
test_file = EpsImagePlugin.EpsImageFile(data) test_file = EpsImagePlugin.EpsImageFile(data)
@ -392,14 +394,14 @@ def test_readline(prefix, line_ending):
"Tests/images/illuCS6_preview.eps", "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 # https://github.com/python-pillow/Pillow/issues/1104
with Image.open(filename) as img: with Image.open(filename) as img:
assert img.mode == "RGB" assert img.mode == "RGB"
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @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 # Test file includes an empty line in the header data
emptyline_file = "Tests/images/zero_bb_emptyline.eps" emptyline_file = "Tests/images/zero_bb_emptyline.eps"
@ -415,14 +417,14 @@ def test_emptyline():
"test_file", "test_file",
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ["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 open(test_file, "rb") as f:
with pytest.raises(Image.UnidentifiedImageError): with pytest.raises(Image.UnidentifiedImageError):
with Image.open(f): with Image.open(f):
pass pass
def test_bounding_box_in_trailer(): def test_bounding_box_in_trailer() -> None:
# Check bounding boxes are parsed in the same way # Check bounding boxes are parsed in the same way
# when specified in the header and the trailer # when specified in the header and the trailer
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open( 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 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 pytest.raises(OSError):
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
pass pass

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/hopper.fits" TEST_FILE = "Tests/images/hopper.fits"
def test_open(): def test_open() -> None:
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
@ -21,7 +22,7 @@ def test_open():
assert_image_equal(im, hopper("L")) assert_image_equal(im, hopper("L"))
def test_invalid_file(): def test_invalid_file() -> None:
# Arrange # Arrange
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
@ -30,14 +31,14 @@ def test_invalid_file():
FitsImagePlugin.FitsImageFile(invalid_file) FitsImagePlugin.FitsImageFile(invalid_file)
def test_truncated_fits(): def test_truncated_fits() -> None:
# No END to headers # No END to headers
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
with pytest.raises(OSError): with pytest.raises(OSError):
FitsImagePlugin.FitsImageFile(BytesIO(image_data)) FitsImagePlugin.FitsImageFile(BytesIO(image_data))
def test_naxis_zero(): def test_naxis_zero() -> None:
# This test image has been manually hexedited # This test image has been manually hexedited
# to set the number of data axes to zero # to set the number of data axes to zero
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -45,7 +46,7 @@ def test_naxis_zero():
pass pass
def test_comment(): def test_comment() -> None:
image_data = b"SIMPLE = T / comment string" image_data = b"SIMPLE = T / comment string"
with pytest.raises(OSError): with pytest.raises(OSError):
FitsImagePlugin.FitsImageFile(BytesIO(image_data)) FitsImagePlugin.FitsImageFile(BytesIO(image_data))

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
import pytest import pytest
@ -15,7 +16,7 @@ static_test_file = "Tests/images/hopper.fli"
animated_test_file = "Tests/images/a.fli" animated_test_file = "Tests/images/a.fli"
def test_sanity(): def test_sanity() -> None:
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
im.load() im.load()
assert im.mode == "P" assert im.mode == "P"
@ -32,8 +33,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(): def test_unclosed_file() -> None:
def open(): def open() -> None:
im = Image.open(static_test_file) im = Image.open(static_test_file)
im.load() im.load()
@ -41,14 +42,14 @@ def test_unclosed_file():
open() open()
def test_closed_file(): def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
im = Image.open(static_test_file) im = Image.open(static_test_file)
im.load() im.load()
im.close() im.close()
def test_seek_after_close(): def test_seek_after_close() -> None:
im = Image.open(animated_test_file) im = Image.open(animated_test_file)
im.seek(1) im.seek(1)
im.close() im.close()
@ -57,13 +58,13 @@ def test_seek_after_close():
im.seek(0) im.seek(0)
def test_context_manager(): def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
im.load() im.load()
def test_tell(): def test_tell() -> None:
# Arrange # Arrange
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
# Act # Act
@ -73,20 +74,20 @@ def test_tell():
assert frame == 0 assert frame == 0
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
FliImagePlugin.FliImageFile(invalid_file) 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("Tests/images/hopper_palette_chunk_second.fli") as im:
with Image.open(static_test_file) as expected: with Image.open(static_test_file) as expected:
assert_image_equal(im.convert("RGB"), expected.convert("RGB")) 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: with Image.open(static_test_file) as im:
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
@ -96,7 +97,7 @@ def test_n_frames():
assert im.is_animated assert im.is_animated
def test_eoferror(): def test_eoferror() -> None:
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
n_frames = im.n_frames n_frames = im.n_frames
@ -109,7 +110,7 @@ def test_eoferror():
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_seek_tell(): def test_seek_tell() -> None:
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
layer_number = im.tell() layer_number = im.tell()
assert layer_number == 0 assert layer_number == 0
@ -131,7 +132,7 @@ def test_seek_tell():
assert layer_number == 1 assert layer_number == 1
def test_seek(): def test_seek() -> None:
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
im.seek(50) im.seek(50)
@ -146,7 +147,7 @@ def test_seek():
], ],
) )
@pytest.mark.timeout(timeout=3) @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 open(test_file, "rb") as f:
with Image.open(f) as im: with Image.open(f) as im:
with pytest.raises(OSError): with pytest.raises(OSError):
@ -159,7 +160,7 @@ def test_timeouts(test_file):
"Tests/images/crash-5762152299364352.fli", "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 open(test_file, "rb") as f:
with Image.open(f) as im: with Image.open(f) as im:
with pytest.raises(OSError): with pytest.raises(OSError):

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image 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: with Image.open("Tests/images/input_bw_one_band.fpx") as im:
assert im.mode == "L" assert im.mode == "L"
assert im.size == (70, 46) assert im.size == (70, 46)
@ -19,7 +20,7 @@ def test_sanity():
assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") 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: with Image.open("Tests/images/input_bw_one_band.fpx") as im:
pass pass
assert im.ole.fp.closed assert im.ole.fp.closed
@ -29,7 +30,7 @@ def test_close():
assert im.ole.fp.closed assert im.ole.fp.closed
def test_invalid_file(): def test_invalid_file() -> None:
# Test an invalid OLE file # Test an invalid OLE file
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
@ -41,7 +42,7 @@ def test_invalid_file():
FpxImagePlugin.FpxImageFile(ole_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 pytest.raises(OSError, match="Invalid number of bands"):
with Image.open("Tests/images/input_bw_five_bands.fpx"): with Image.open("Tests/images/input_bw_five_bands.fpx"):
pass pass

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import FtexImagePlugin, Image from PIL import FtexImagePlugin, Image
@ -6,18 +7,18 @@ from PIL import FtexImagePlugin, Image
from .helper import assert_image_equal_tofile, assert_image_similar 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: with Image.open("Tests/images/ftex_uncompressed.ftu") as im:
assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png") 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.ftc") as im:
with Image.open("Tests/images/ftex_dxt1.png") as target: with Image.open("Tests/images/ftex_dxt1.png") as target:
assert_image_similar(im, target.convert("RGBA"), 15) assert_image_similar(im, target.convert("RGBA"), 15)
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import GbrImagePlugin, Image from PIL import GbrImagePlugin, Image
@ -6,12 +7,12 @@ from PIL import GbrImagePlugin, Image
from .helper import assert_image_equal_tofile 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: with Image.open("Tests/images/gbr.gbr") as im:
assert_image_equal_tofile(im, "Tests/images/gbr.png") 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: with Image.open("Tests/images/gbr.gbr") as im:
assert im.load()[0, 0] == (0, 0, 0, 0) 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) 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: with Image.open("Tests/images/gbr.gbr") as im:
im.load() im.load()
im.load() im.load()
assert_image_equal_tofile(im, "Tests/images/gbr.png") assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import GdImageFile, UnidentifiedImageError from PIL import GdImageFile, UnidentifiedImageError
@ -6,18 +7,18 @@ from PIL import GdImageFile, UnidentifiedImageError
TEST_GD_FILE = "Tests/images/hopper.gd" TEST_GD_FILE = "Tests/images/hopper.gd"
def test_sanity(): def test_sanity() -> None:
with GdImageFile.open(TEST_GD_FILE) as im: with GdImageFile.open(TEST_GD_FILE) as im:
assert im.size == (128, 128) assert im.size == (128, 128)
assert im.format == "GD" assert im.format == "GD"
def test_bad_mode(): def test_bad_mode() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
GdImageFile.open(TEST_GD_FILE, "bad mode") GdImageFile.open(TEST_GD_FILE, "bad mode")
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):

View File

@ -1,6 +1,9 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import Generator
import pytest import pytest
@ -22,7 +25,7 @@ with open(TEST_GIF, "rb") as f:
data = f.read() data = f.read()
def test_sanity(): def test_sanity() -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im.load() im.load()
assert im.mode == "P" assert im.mode == "P"
@ -32,8 +35,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(): def test_unclosed_file() -> None:
def open(): def open() -> None:
im = Image.open(TEST_GIF) im = Image.open(TEST_GIF)
im.load() im.load()
@ -41,14 +44,14 @@ def test_unclosed_file():
open() open()
def test_closed_file(): def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
im = Image.open(TEST_GIF) im = Image.open(TEST_GIF)
im.load() im.load()
im.close() im.close()
def test_seek_after_close(): def test_seek_after_close() -> None:
im = Image.open("Tests/images/iss634.gif") im = Image.open("Tests/images/iss634.gif")
im.load() im.load()
im.close() im.close()
@ -61,20 +64,20 @@ def test_seek_after_close():
im.seek(1) im.seek(1)
def test_context_manager(): def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im.load() im.load()
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
GifImagePlugin.GifImageFile(invalid_file) 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: with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
assert im.mode == "L" assert im.mode == "L"
assert im.load()[0, 0] == 128 assert im.load()[0, 0] == 128
@ -85,7 +88,7 @@ def test_l_mode_transparency():
assert im.load()[0, 0] == 128 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: with Image.open("Tests/images/no_palette_after_rgb.gif") as im:
im.seek(1) im.seek(1)
assert im.mode == "RGB" assert im.mode == "RGB"
@ -94,13 +97,13 @@ def test_l_mode_after_rgb():
assert im.mode == "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: with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im:
im.seek(1) im.seek(1)
assert_image_similar(im, hopper("L").convert("RGB"), 8) 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: with Image.open("Tests/images/iss634.gif") as im:
expected_rgb_always = im.convert("RGB") expected_rgb_always = im.convert("RGB")
@ -141,14 +144,14 @@ def test_strategy():
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
def test_optimize(): def test_optimize() -> None:
def test_grayscale(optimize): def test_grayscale(optimize: int) -> int:
im = Image.new("L", (1, 1), 0) im = Image.new("L", (1, 1), 0)
filename = BytesIO() filename = BytesIO()
im.save(filename, "GIF", optimize=optimize) im.save(filename, "GIF", optimize=optimize)
return len(filename.getvalue()) return len(filename.getvalue())
def test_bilevel(optimize): def test_bilevel(optimize: int) -> int:
im = Image.new("1", (1, 1), 0) im = Image.new("1", (1, 1), 0)
test_file = BytesIO() test_file = BytesIO()
im.save(test_file, "GIF", optimize=optimize) im.save(test_file, "GIF", optimize=optimize)
@ -176,7 +179,9 @@ def test_optimize():
(4, 513, 256), (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. # 256 color Palette image, posterize to > 128 and < 128 levels.
# Size bigger and smaller than 512x512. # Size bigger and smaller than 512x512.
# Check the palette for number of colors allocated. # 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")) 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))) im = Image.frombytes("L", (16, 16), bytes(range(256)))
test_file = BytesIO() test_file = BytesIO()
im.save(test_file, "GIF", optimize=True) im.save(test_file, "GIF", optimize=True)
assert im.mode == "L" 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 = Image.new("P", (8, 1))
im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150)) im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
for i in range(8): 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 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") out = str(tmp_path / "temp.gif")
im = Image.new("P", (1, 256)) im = Image.new("P", (1, 256))
@ -239,7 +244,7 @@ def test_full_palette_second_frame(tmp_path):
reloaded.getpixel((0, i)) == i reloaded.getpixel((0, i)) == i
def test_roundtrip(tmp_path): def test_roundtrip(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im = hopper() im = hopper()
im.save(out) im.save(out)
@ -247,7 +252,7 @@ def test_roundtrip(tmp_path):
assert_image_similar(reread.convert("RGB"), im, 50) 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 # see https://github.com/python-pillow/Pillow/issues/403
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
@ -257,7 +262,7 @@ def test_roundtrip2(tmp_path):
assert_image_similar(reread.convert("RGB"), hopper(), 50) 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 # Single frame image
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im = hopper() im = hopper()
@ -274,7 +279,7 @@ def test_roundtrip_save_all(tmp_path):
assert reread.n_frames == 5 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") out = str(tmp_path / "temp.gif")
im = Image.new("1", (1, 1)) im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1) im2 = Image.new("1", (1, 1), 1)
@ -295,7 +300,7 @@ def test_roundtrip_save_all_1(tmp_path):
("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), ("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: with Image.open(path) as im:
assert im.mode == "P" assert im.mode == "P"
first_frame_colors = im.palette.colors.keys() 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 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"] important_headers = ["background", "version", "duration", "loop"]
# Multiframe image # Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im: 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] 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 # see https://github.com/python-pillow/Pillow/issues/513
with Image.open(TEST_GIF) as im: 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) 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 # 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") out = str(tmp_path / "temp.gif")
im.copy().save(out, *args, **kwargs) im.copy().save(out, **kwargs)
reloaded = Image.open(out) reloaded = Image.open(out)
return reloaded return reloaded
@ -367,7 +372,7 @@ def test_palette_434(tmp_path):
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") @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: with Image.open(TEST_GIF) as img:
img = img.convert("RGB") 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") @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: with Image.open(TEST_GIF) as img:
img = img.convert("L") img = img.convert("L")
@ -388,7 +393,7 @@ def test_save_netpbm_l_mode(tmp_path):
assert_image_similar(img, reloaded.convert("L"), 0) 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: with Image.open("Tests/images/dispose_none.gif") as img:
frame_count = 0 frame_count = 0
try: try:
@ -399,7 +404,7 @@ def test_seek():
assert frame_count == 5 assert frame_count == 5
def test_seek_info(): def test_seek_info() -> None:
with Image.open("Tests/images/iss634.gif") as im: with Image.open("Tests/images/iss634.gif") as im:
info = im.info.copy() info = im.info.copy()
@ -409,7 +414,7 @@ def test_seek_info():
assert im.info == info assert im.info == info
def test_seek_rewind(): def test_seek_rewind() -> None:
with Image.open("Tests/images/iss634.gif") as im: with Image.open("Tests/images/iss634.gif") as im:
im.seek(2) im.seek(2)
im.seek(1) im.seek(1)
@ -427,7 +432,7 @@ def test_seek_rewind():
("Tests/images/iss634.gif", 42), ("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 # Test is_animated before n_frames
with Image.open(path) as im: with Image.open(path) as im:
assert im.is_animated == (n_frames != 1) 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) assert im.is_animated == (n_frames != 1)
def test_no_change(): def test_no_change() -> None:
# Test n_frames does not change the image # Test n_frames does not change the image
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(1) im.seek(1)
@ -459,7 +464,7 @@ def test_no_change():
assert_image_equal(im, expected) assert_image_equal(im, expected)
def test_eoferror(): def test_eoferror() -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
n_frames = im.n_frames n_frames = im.n_frames
@ -472,13 +477,13 @@ def test_eoferror():
im.seek(n_frames - 1) 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: with Image.open("Tests/images/first_frame_transparency.gif") as im:
px = im.load() px = im.load()
assert px[0, 0] == im.info["transparency"] 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: with Image.open("Tests/images/dispose_none.gif") as img:
try: try:
while True: while True:
@ -488,7 +493,7 @@ def test_dispose_none():
pass pass
def test_dispose_none_load_end(): def test_dispose_none_load_end() -> None:
# Test image created with: # Test image created with:
# #
# im = Image.open("transparent.gif") # 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") 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: with Image.open("Tests/images/dispose_bgnd.gif") as img:
try: try:
while True: while True:
@ -511,7 +516,7 @@ def test_dispose_background():
pass pass
def test_dispose_background_transparency(): def test_dispose_background_transparency() -> None:
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
img.seek(2) img.seek(2)
px = img.load() 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 GifImagePlugin.LOADING_STRATEGY = loading_strategy
try: try:
with Image.open("Tests/images/transparent_dispose.gif") as img: 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 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: with Image.open("Tests/images/dispose_prev.gif") as img:
try: try:
while True: while True:
@ -562,7 +570,7 @@ def test_dispose_previous():
pass 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: with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
im.seek(1) im.seek(1)
assert_image_equal_tofile( 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: with Image.open("Tests/images/dispose_none.gif") as img:
img.load() img.load()
img.seek(1) img.seek(1)
@ -581,7 +589,7 @@ def test_previous_frame_loaded():
assert_image_equal(img_skipped, img) 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") out = str(tmp_path / "temp.gif")
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
@ -609,7 +617,7 @@ def test_save_dispose(tmp_path):
assert img.disposal_method == i + 1 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") out = str(tmp_path / "temp.gif")
# Four colors: white, gray, black, red # Four colors: white, gray, black, red
@ -640,7 +648,7 @@ def test_dispose2_palette(tmp_path):
assert rgb_img.getpixel((50, 50)) == circle 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") out = str(tmp_path / "temp.gif")
# 4 frames: red/blue, red/red, blue/blue, red/blue # 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) 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") out = str(tmp_path / "temp.gif")
im_list = [] im_list = []
@ -708,7 +716,7 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0) 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") out = str(tmp_path / "temp.gif")
im_list = [Image.new("RGBA", (1, 20))] im_list = [Image.new("RGBA", (1, 20))]
@ -726,7 +734,7 @@ def test_dispose2_background_frame(tmp_path):
assert im.n_frames == 3 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") out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im: with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0 assert im.info["transparency"] == 0
@ -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: with Image.open("Tests/images/iss634.gif") as img:
# Seek to the second frame # Seek to the second frame
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
@ -756,7 +764,7 @@ def test_no_transparency_in_second_frame():
assert img.histogram()[255] == 0 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") out = str(tmp_path / "temp.gif")
im = Image.new("P", (1, 2)) im = Image.new("P", (1, 2))
@ -772,7 +780,7 @@ def test_remapped_transparency(tmp_path):
assert reloaded.info["transparency"] == reloaded.getpixel((0, 1)) assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
def test_duration(tmp_path): def test_duration(tmp_path: Path) -> None:
duration = 1000 duration = 1000
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -786,7 +794,7 @@ def test_duration(tmp_path):
assert reread.info["duration"] == duration assert reread.info["duration"] == duration
def test_multiple_duration(tmp_path): def test_multiple_duration(tmp_path: Path) -> None:
duration_list = [1000, 2000, 3000] duration_list = [1000, 2000, 3000]
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -821,7 +829,7 @@ def test_multiple_duration(tmp_path):
pass pass
def test_roundtrip_info_duration(tmp_path): def test_roundtrip_info_duration(tmp_path: Path) -> None:
duration_list = [100, 500, 500] duration_list = [100, 500, 500]
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -838,7 +846,7 @@ def test_roundtrip_info_duration(tmp_path):
] == duration_list ] == 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") out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/duplicate_frame.gif") as im: with Image.open("Tests/images/duplicate_frame.gif") as im:
assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
@ -854,7 +862,7 @@ def test_roundtrip_info_duration_combined(tmp_path):
] == [1000, 2000] ] == [1000, 2000]
def test_identical_frames(tmp_path): def test_identical_frames(tmp_path: Path) -> None:
duration_list = [1000, 1500, 2000, 4000] duration_list = [1000, 1500, 2000, 4000]
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -887,7 +895,9 @@ def test_identical_frames(tmp_path):
1500, 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") out = str(tmp_path / "temp.gif")
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), 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 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") out = str(tmp_path / "temp.gif")
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.save(out, loop=None) im.save(out, loop=None)
@ -912,7 +922,7 @@ def test_loop_none(tmp_path):
assert "loop" not in reread.info 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 number_of_loops = 2
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -930,7 +940,7 @@ def test_number_of_loops(tmp_path):
assert im.info["loop"] == 2 assert im.info["loop"] == 2
def test_background(tmp_path): def test_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.info["background"] = 1 im.info["background"] = 1
@ -939,7 +949,7 @@ def test_background(tmp_path):
assert reread.info["background"] == im.info["background"] 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") out = str(tmp_path / "temp.gif")
# Test opaque WebP background # Test opaque WebP background
@ -954,7 +964,7 @@ def test_webp_background(tmp_path):
im.save(out) im.save(out)
def test_comment(tmp_path): def test_comment(tmp_path: Path) -> None:
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0"
@ -974,7 +984,7 @@ def test_comment(tmp_path):
assert reread.info["version"] == b"GIF89a" 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") out = str(tmp_path / "temp.gif")
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
comment = b"Test comment text" comment = b"Test comment text"
@ -989,18 +999,18 @@ def test_comment_over_255(tmp_path):
assert reread.info["version"] == b"GIF89a" 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: with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
assert_image_equal_tofile(im, TEST_GIF) 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: with Image.open("Tests/images/multiple_comments.gif") as im:
# Multiple comment blocks in a frame are separated not concatenated # Multiple comment blocks in a frame are separated not concatenated
assert im.info["comment"] == b"Test comment 1\nTest comment 2" 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") out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
assert "comment" in im.info assert "comment" in im.info
@ -1013,7 +1023,7 @@ def test_empty_string_comment(tmp_path):
assert "comment" not in frame.info 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 # Test that a comment block at the beginning is kept
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
for frame in ImageSequence.Iterator(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" assert frame.info["comment"] == b"Test"
def test_version(tmp_path): def test_version(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") 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) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["version"] == version assert reread.info["version"] == version
@ -1074,7 +1084,7 @@ def test_version(tmp_path):
assert_version_after_save(im, b"GIF87a") 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") out = str(tmp_path / "temp.gif")
# Test appending single frame images # Test appending single frame images
@ -1086,7 +1096,7 @@ def test_append_images(tmp_path):
assert reread.n_frames == 3 assert reread.n_frames == 3
# Tests appending using a generator # Tests appending using a generator
def im_generator(ims): def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
yield from ims yield from ims
im.save(out, save_all=True, append_images=im_generator(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 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 # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
# transparency. # transparency.
# Need a palette that isn't using the 0 color, # 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)) 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") out = str(tmp_path / "temp.gif")
im = Image.new("RGB", (256, 1)) im = Image.new("RGB", (256, 1))
@ -1138,7 +1148,7 @@ def test_removed_transparency(tmp_path):
assert "transparency" not in reloaded.info 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") out = str(tmp_path / "temp.gif")
# Single frame # Single frame
@ -1160,7 +1170,7 @@ def test_rgb_transparency(tmp_path):
assert "transparency" not in reloaded.info 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") out = str(tmp_path / "temp.gif")
im = hopper("P") im = hopper("P")
@ -1171,13 +1181,13 @@ def test_rgba_transparency(tmp_path):
assert_image_equal(hopper("P").convert("RGB"), reloaded) 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: with Image.open("Tests/images/background_outside_palette.gif") as im:
im.seek(1) im.seek(1)
assert im.info["background"] == 255 assert im.info["background"] == 255
def test_bbox(tmp_path): def test_bbox(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im = Image.new("RGB", (100, 100), "#fff") im = Image.new("RGB", (100, 100), "#fff")
@ -1188,7 +1198,7 @@ def test_bbox(tmp_path):
assert reread.n_frames == 2 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") out = str(tmp_path / "temp.gif")
im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) 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 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 # Generate an L mode image with a separate palette
im = hopper("P") im = hopper("P")
@ -1214,7 +1224,7 @@ def test_palette_save_L(tmp_path):
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) 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 = Image.new("P", (1, 2))
im.putpixel((0, 1), 1) 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) 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 = Image.new("P", (1, 2))
im.putpixel((0, 1), 1) 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) 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 = [] frames = []
colors = ((255, 0, 0), (0, 255, 0)) colors = ((255, 0, 0), (0, 255, 0))
for color in colors: for color in colors:
@ -1264,7 +1274,7 @@ def test_palette_save_all_P(tmp_path):
assert im.palette.palette == im.global_palette.palette 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 # Pass in a different palette, as an ImagePalette.ImagePalette
# effectively the same as test_palette_save_P # 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")) 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' # Test saving something that would trigger the auto-convert to 'L'
im = hopper("I") im = hopper("I")
@ -1291,7 +1301,7 @@ def test_save_I(tmp_path):
assert_image_equal(reloaded.convert("L"), im.convert("L")) assert_image_equal(reloaded.convert("L"), im.convert("L"))
def test_getdata(): def test_getdata() -> None:
# Test getheader/getdata against legacy values. # Test getheader/getdata against legacy values.
# Create a 'P' image with holes in the palette. # Create a 'P' image with holes in the palette.
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
@ -1319,7 +1329,7 @@ def test_getdata():
GifImagePlugin._FORCE_OPTIMIZE = False GifImagePlugin._FORCE_OPTIMIZE = False
def test_lzw_bits(): def test_lzw_bits() -> None:
# see https://github.com/python-pillow/Pillow/issues/2811 # see https://github.com/python-pillow/Pillow/issues/2811
with Image.open("Tests/images/issue_2811.gif") as im: with Image.open("Tests/images/issue_2811.gif") as im:
assert im.tile[0][3][0] == 11 # LZW bits assert im.tile[0][3][0] == 11 # LZW bits
@ -1327,7 +1337,7 @@ def test_lzw_bits():
im.load() im.load()
def test_extents(): def test_extents() -> None:
with Image.open("Tests/images/test_extents.gif") as im: with Image.open("Tests/images/test_extents.gif") as im:
assert im.size == (100, 100) assert im.size == (100, 100)
@ -1339,7 +1349,7 @@ def test_extents():
assert im.size == (150, 150) 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, # The Global Color Table Flag isn't set, so there is no background color index,
# but the disposal method is "Restore to background color" # but the disposal method is "Restore to background color"
with Image.open("Tests/images/missing_background.gif") as im: 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") 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") out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/transparent.png") as im: with Image.open("Tests/images/transparent.png") as im:
im.save(out) im.save(out)

View File

@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
from PIL import GimpGradientFile, ImagePalette from PIL import GimpGradientFile, ImagePalette
def test_linear_pos_le_middle(): def test_linear_pos_le_middle() -> None:
# Arrange # Arrange
middle = 0.5 middle = 0.5
pos = 0.25 pos = 0.25
@ -14,7 +15,7 @@ def test_linear_pos_le_middle():
assert ret == 0.25 assert ret == 0.25
def test_linear_pos_le_small_middle(): def test_linear_pos_le_small_middle() -> None:
# Arrange # Arrange
middle = 1e-11 middle = 1e-11
pos = 1e-12 pos = 1e-12
@ -26,7 +27,7 @@ def test_linear_pos_le_small_middle():
assert ret == 0.0 assert ret == 0.0
def test_linear_pos_gt_middle(): def test_linear_pos_gt_middle() -> None:
# Arrange # Arrange
middle = 0.5 middle = 0.5
pos = 0.75 pos = 0.75
@ -38,7 +39,7 @@ def test_linear_pos_gt_middle():
assert ret == 0.75 assert ret == 0.75
def test_linear_pos_gt_small_middle(): def test_linear_pos_gt_small_middle() -> None:
# Arrange # Arrange
middle = 1 - 1e-11 middle = 1 - 1e-11
pos = 1 - 1e-12 pos = 1 - 1e-12
@ -50,7 +51,7 @@ def test_linear_pos_gt_small_middle():
assert ret == 1.0 assert ret == 1.0
def test_curved(): def test_curved() -> None:
# Arrange # Arrange
middle = 0.5 middle = 0.5
pos = 0.75 pos = 0.75
@ -62,7 +63,7 @@ def test_curved():
assert ret == 0.75 assert ret == 0.75
def test_sine(): def test_sine() -> None:
# Arrange # Arrange
middle = 0.5 middle = 0.5
pos = 0.75 pos = 0.75
@ -74,7 +75,7 @@ def test_sine():
assert ret == 0.8535533905932737 assert ret == 0.8535533905932737
def test_sphere_increasing(): def test_sphere_increasing() -> None:
# Arrange # Arrange
middle = 0.5 middle = 0.5
pos = 0.75 pos = 0.75
@ -86,7 +87,7 @@ def test_sphere_increasing():
assert round(abs(ret - 0.9682458365518543), 7) == 0 assert round(abs(ret - 0.9682458365518543), 7) == 0
def test_sphere_decreasing(): def test_sphere_decreasing() -> None:
# Arrange # Arrange
middle = 0.5 middle = 0.5
pos = 0.75 pos = 0.75
@ -98,7 +99,7 @@ def test_sphere_decreasing():
assert ret == 0.3385621722338523 assert ret == 0.3385621722338523
def test_load_via_imagepalette(): def test_load_via_imagepalette() -> None:
# Arrange # Arrange
test_file = "Tests/images/gimp_gradient.ggr" test_file = "Tests/images/gimp_gradient.ggr"
@ -111,7 +112,7 @@ def test_load_via_imagepalette():
assert palette[1] == "RGBA" assert palette[1] == "RGBA"
def test_load_1_3_via_imagepalette(): def test_load_1_3_via_imagepalette() -> None:
# Arrange # Arrange
# GIMP 1.3 gradient files contain a name field # GIMP 1.3 gradient files contain a name field
test_file = "Tests/images/gimp_gradient_with_name.ggr" test_file = "Tests/images/gimp_gradient_with_name.ggr"

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL.GimpPaletteFile import GimpPaletteFile from PIL.GimpPaletteFile import GimpPaletteFile
def test_sanity(): def test_sanity() -> None:
with open("Tests/images/test.gpl", "rb") as fp: with open("Tests/images/test.gpl", "rb") as fp:
GimpPaletteFile(fp) GimpPaletteFile(fp)
@ -21,7 +22,7 @@ def test_sanity():
GimpPaletteFile(fp) GimpPaletteFile(fp)
def test_get_palette(): def test_get_palette() -> None:
# Arrange # Arrange
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
palette_file = GimpPaletteFile(fp) palette_file = GimpPaletteFile(fp)

View File

@ -1,4 +1,8 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from typing import IO
import pytest import pytest
from PIL import GribStubImagePlugin, Image from PIL import GribStubImagePlugin, Image
@ -8,7 +12,7 @@ from .helper import hopper
TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" TEST_FILE = "Tests/images/WAlaska.wind.7days.grb"
def test_open(): def test_open() -> None:
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
@ -19,7 +23,7 @@ def test_open():
assert im.size == (1, 1) assert im.size == (1, 1)
def test_invalid_file(): def test_invalid_file() -> None:
# Arrange # Arrange
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
@ -28,7 +32,7 @@ def test_invalid_file():
GribStubImagePlugin.GribStubImageFile(invalid_file) GribStubImagePlugin.GribStubImageFile(invalid_file)
def test_load(): def test_load() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler # Act / Assert: stub cannot load without an implemented handler
@ -36,7 +40,7 @@ def test_load():
im.load() im.load()
def test_save(tmp_path): def test_save(tmp_path: Path) -> None:
# Arrange # Arrange
im = hopper() im = hopper()
tmpfile = str(tmp_path / "temp.grib") tmpfile = str(tmp_path / "temp.grib")
@ -46,21 +50,21 @@ def test_save(tmp_path):
im.save(tmpfile) im.save(tmpfile)
def test_handler(tmp_path): def test_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler:
opened = False opened = False
loaded = False loaded = False
saved = False saved = False
def open(self, im): def open(self, im: Image.Image) -> None:
self.opened = True self.opened = True
def load(self, im): def load(self, im: Image.Image) -> Image.Image:
self.loaded = True self.loaded = True
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) 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 self.saved = True
handler = TestHandler() handler = TestHandler()

View File

@ -1,4 +1,8 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from typing import IO
import pytest import pytest
from PIL import Hdf5StubImagePlugin, Image from PIL import Hdf5StubImagePlugin, Image
@ -6,7 +10,7 @@ from PIL import Hdf5StubImagePlugin, Image
TEST_FILE = "Tests/images/hdf5.h5" TEST_FILE = "Tests/images/hdf5.h5"
def test_open(): def test_open() -> None:
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
@ -17,7 +21,7 @@ def test_open():
assert im.size == (1, 1) assert im.size == (1, 1)
def test_invalid_file(): def test_invalid_file() -> None:
# Arrange # Arrange
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
@ -26,7 +30,7 @@ def test_invalid_file():
Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file)
def test_load(): def test_load() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler # Act / Assert: stub cannot load without an implemented handler
@ -34,7 +38,7 @@ def test_load():
im.load() im.load()
def test_save(): def test_save() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
dummy_fp = None dummy_fp = None
@ -47,21 +51,21 @@ def test_save():
Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename)
def test_handler(tmp_path): def test_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler:
opened = False opened = False
loaded = False loaded = False
saved = False saved = False
def open(self, im): def open(self, im: Image.Image) -> None:
self.opened = True self.opened = True
def load(self, im): def load(self, im: Image.Image) -> Image.Image:
self.loaded = True self.loaded = True
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) 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 self.saved = True
handler = TestHandler() handler = TestHandler()

View File

@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
import io import io
import os import os
import warnings import warnings
from pathlib import Path
import pytest import pytest
@ -13,7 +15,7 @@ from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless
TEST_FILE = "Tests/images/pillow.icns" TEST_FILE = "Tests/images/pillow.icns"
def test_sanity(): def test_sanity() -> None:
# Loading this icon by default should result in the largest size # Loading this icon by default should result in the largest size
# (512x512@2x) being loaded # (512x512@2x) being loaded
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
@ -26,7 +28,7 @@ def test_sanity():
assert im.format == "ICNS" assert im.format == "ICNS"
def test_load(): def test_load() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.load()[0, 0] == (0, 0, 0, 0) 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) 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") temp_file = str(tmp_path / "temp.icns")
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
@ -51,7 +53,7 @@ def test_save(tmp_path):
assert _binary.i32be(fp.read(4)) == file_length 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") temp_file = str(tmp_path / "temp.icns")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128))
@ -66,7 +68,7 @@ def test_save_append_images(tmp_path):
assert_image_equal(reread, provided_im) assert_image_equal(reread, provided_im)
def test_save_fp(): def test_save_fp() -> None:
fp = io.BytesIO() fp = io.BytesIO()
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
@ -78,7 +80,7 @@ def test_save_fp():
assert reread.format == "ICNS" 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 # Check that we can load all of the sizes, and that the final pixel
# dimensions are as expected # dimensions are as expected
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
@ -95,7 +97,7 @@ def test_sizes():
im.size = (1, 1) 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 # 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). # uses PNG rather than JP2, however (since it was made on 10.9).
with Image.open("Tests/images/pillow2.icns") as im: with Image.open("Tests/images/pillow2.icns") as im:
@ -110,7 +112,7 @@ def test_older_icon():
@skip_unless_feature("jpg_2000") @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. # 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 # The advantage of doing this is that OS X 10.5 supports JPEG 2000
# but not PNG; some commercial software therefore does just this. # but not PNG; some commercial software therefore does just this.
@ -126,7 +128,7 @@ def test_jp2_icon():
assert im2.size == (wr, hr) assert im2.size == (wr, hr)
def test_getimage(): def test_getimage() -> None:
with open(TEST_FILE, "rb") as fp: with open(TEST_FILE, "rb") as fp:
icns_file = IcnsImagePlugin.IcnsFile(fp) icns_file = IcnsImagePlugin.IcnsFile(fp)
@ -139,14 +141,14 @@ def test_getimage():
assert im.size == (512, 512) 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 io.BytesIO(b"invalid\n") as fp:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
IcnsImagePlugin.IcnsFile(fp) IcnsImagePlugin.IcnsFile(fp)
@skip_unless_feature("jpg_2000") @skip_unless_feature("jpg_2000")
def test_icns_decompression_bomb(): def test_icns_decompression_bomb() -> None:
with Image.open( with Image.open(
"Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns" "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns"
) as im: ) as im:

View File

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import io import io
import os import os
from pathlib import Path
import pytest import pytest
@ -11,7 +13,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper
TEST_ICO_FILE = "Tests/images/hopper.ico" TEST_ICO_FILE = "Tests/images/hopper.ico"
def test_sanity(): def test_sanity() -> None:
with Image.open(TEST_ICO_FILE) as im: with Image.open(TEST_ICO_FILE) as im:
im.load() im.load()
assert im.mode == "RGBA" assert im.mode == "RGBA"
@ -20,29 +22,29 @@ def test_sanity():
assert im.get_format_mimetype() == "image/x-icon" assert im.get_format_mimetype() == "image/x-icon"
def test_load(): def test_load() -> None:
with Image.open(TEST_ICO_FILE) as im: with Image.open(TEST_ICO_FILE) as im:
assert im.load()[0, 0] == (1, 1, 9, 255) 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: with Image.open("Tests/images/hopper_mask.ico") as im:
assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") 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: with Image.open("Tests/images/black_and_white.ico") as im:
assert im.mode == "RGBA" assert im.mode == "RGBA"
assert im.size == (16, 16) assert im.size == (16, 16)
def test_invalid_file(): def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp: with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
IcoImagePlugin.IcoImageFile(fp) IcoImagePlugin.IcoImageFile(fp)
def test_save_to_bytes(): def test_save_to_bytes() -> None:
output = io.BytesIO() output = io.BytesIO()
im = hopper() im = hopper()
im.save(output, "ico", sizes=[(32, 32), (64, 64)]) 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") temp_file = str(tmp_path / "temp.ico")
im = hopper() im = hopper()
@ -85,7 +87,7 @@ def test_getpixel(tmp_path):
assert reloaded.getpixel((0, 0)) == (18, 20, 62) 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_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.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) 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_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.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")) @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
def test_save_to_bytes_bmp(mode): def test_save_to_bytes_bmp(mode) -> None:
output = io.BytesIO() output = io.BytesIO()
im = hopper(mode) im = hopper(mode)
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) 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) assert_image_equal(reloaded, im)
def test_incorrect_size(): def test_incorrect_size() -> None:
with Image.open(TEST_ICO_FILE) as im: with Image.open(TEST_ICO_FILE) as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.size = (1, 1) 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""" """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264"""
# Arrange # Arrange
with Image.open("Tests/images/hopper_256x256.ico") as im: with Image.open("Tests/images/hopper_256x256.ico") as im:
@ -180,7 +182,7 @@ def test_save_256x256(tmp_path):
assert im_saved.size == (256, 256) 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 """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266
Should save in 16x16, 24x24, 32x32, 48x48 sizes Should save in 16x16, 24x24, 32x32, 48x48 sizes
and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 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)} 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 # append_images should be used for scaled down versions of the image
im = hopper("RGBA") im = hopper("RGBA")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) provided_im = Image.new("RGBA", (32, 32), (255, 0, 0))
@ -210,7 +212,7 @@ def test_save_append_images(tmp_path):
assert_image_equal(reread, provided_im) 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 # This image has been manually hexedited to state that it is 16x32
# while the image within is still 16x16 # while the image within is still 16x16
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
@ -218,7 +220,7 @@ def test_unexpected_size():
assert im.size == (16, 16) 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: with Image.open(TEST_ICO_FILE) as im:
outfile = str(tmp_path / "temp_saved_hopper_draw.ico") outfile = str(tmp_path / "temp_saved_hopper_draw.ico")

View File

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import filecmp import filecmp
import warnings import warnings
from pathlib import Path
import pytest import pytest
@ -12,7 +14,7 @@ from .helper import assert_image_equal_tofile, hopper, is_pypy
TEST_IM = "Tests/images/hopper.im" TEST_IM = "Tests/images/hopper.im"
def test_sanity(): def test_sanity() -> None:
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
im.load() im.load()
assert im.mode == "RGB" assert im.mode == "RGB"
@ -20,7 +22,7 @@ def test_sanity():
assert im.format == "IM" 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")) out = str(tmp_path / ("name_limit_test" * 7 + ".im"))
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
im.save(out) im.save(out)
@ -28,8 +30,8 @@ def test_name_limit(tmp_path):
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(): def test_unclosed_file() -> None:
def open(): def open() -> None:
im = Image.open(TEST_IM) im = Image.open(TEST_IM)
im.load() im.load()
@ -37,20 +39,20 @@ def test_unclosed_file():
open() open()
def test_closed_file(): def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
im = Image.open(TEST_IM) im = Image.open(TEST_IM)
im.load() im.load()
im.close() im.close()
def test_context_manager(): def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
im.load() im.load()
def test_tell(): def test_tell() -> None:
# Arrange # Arrange
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
# Act # Act
@ -60,13 +62,13 @@ def test_tell():
assert frame == 0 assert frame == 0
def test_n_frames(): def test_n_frames() -> None:
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
def test_eoferror(): def test_eoferror() -> None:
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
n_frames = im.n_frames n_frames = im.n_frames
@ -80,14 +82,14 @@ def test_eoferror():
@pytest.mark.parametrize("mode", ("RGB", "P", "PA")) @pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
def test_roundtrip(mode, tmp_path): def test_roundtrip(mode, tmp_path: Path) -> None:
out = str(tmp_path / "temp.im") out = str(tmp_path / "temp.im")
im = hopper(mode) im = hopper(mode)
im.save(out) im.save(out)
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
def test_small_palette(tmp_path): def test_small_palette(tmp_path: Path) -> None:
im = Image.new("P", (1, 1)) im = Image.new("P", (1, 1))
colors = [0, 1, 2] colors = [0, 1, 2]
im.putpalette(colors) im.putpalette(colors)
@ -99,19 +101,19 @@ def test_small_palette(tmp_path):
assert reloaded.getpalette() == colors + [0] * 765 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") out = str(tmp_path / "temp.im")
im = hopper("HSV") im = hopper("HSV")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(out) im.save(out)
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
ImImagePlugin.ImImageFile(invalid_file) ImImagePlugin.ImImageFile(invalid_file)
def test_number(): def test_number() -> None:
assert ImImagePlugin.number("1.2") == 1.2 assert ImImagePlugin.number("1.2") == 1.2

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import pytest import pytest
@ -8,13 +9,13 @@ from PIL import Image, ImtImagePlugin
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
def test_sanity(): def test_sanity() -> None:
with Image.open("Tests/images/bw_gradient.imt") as im: with Image.open("Tests/images/bw_gradient.imt") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n")) @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 io.BytesIO(data) as fp:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
ImtImagePlugin.ImtImageFile(fp) ImtImagePlugin.ImtImageFile(fp)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from io import BytesIO, StringIO from io import BytesIO, StringIO
@ -11,7 +12,7 @@ from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/iptc.jpg" TEST_FILE = "Tests/images/iptc.jpg"
def test_open(): def test_open() -> None:
expected = Image.new("L", (1, 1)) expected = Image.new("L", (1, 1))
f = BytesIO( f = BytesIO(
@ -23,7 +24,7 @@ def test_open():
assert_image_equal(im, expected) assert_image_equal(im, expected)
def test_getiptcinfo_jpg_none(): def test_getiptcinfo_jpg_none() -> None:
# Arrange # Arrange
with hopper() as im: with hopper() as im:
# Act # Act
@ -33,7 +34,7 @@ def test_getiptcinfo_jpg_none():
assert iptc is None assert iptc is None
def test_getiptcinfo_jpg_found(): def test_getiptcinfo_jpg_found() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act # Act
@ -45,7 +46,7 @@ def test_getiptcinfo_jpg_found():
assert iptc[(2, 101)] == b"Hungary" assert iptc[(2, 101)] == b"Hungary"
def test_getiptcinfo_fotostation(): def test_getiptcinfo_fotostation() -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb") as fp: with open(TEST_FILE, "rb") as fp:
data = bytearray(fp.read()) data = bytearray(fp.read())
@ -62,7 +63,7 @@ def test_getiptcinfo_fotostation():
pytest.fail("FotoStation tag not found") pytest.fail("FotoStation tag not found")
def test_getiptcinfo_zero_padding(): def test_getiptcinfo_zero_padding() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.info["photoshop"][0x0404] += b"\x00\x00\x00" im.info["photoshop"][0x0404] += b"\x00\x00\x00"
@ -75,7 +76,7 @@ def test_getiptcinfo_zero_padding():
assert len(iptc) == 3 assert len(iptc) == 3
def test_getiptcinfo_tiff_none(): def test_getiptcinfo_tiff_none() -> None:
# Arrange # Arrange
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
# Act # Act
@ -85,7 +86,7 @@ def test_getiptcinfo_tiff_none():
assert iptc is None assert iptc is None
def test_i(): def test_i() -> None:
# Arrange # Arrange
c = b"a" c = b"a"
@ -97,7 +98,7 @@ def test_i():
assert ret == 97 assert ret == 97
def test_dump(monkeypatch): def test_dump(monkeypatch) -> None:
# Arrange # Arrange
c = b"abc" c = b"abc"
# Temporarily redirect stdout # Temporarily redirect stdout
@ -112,6 +113,6 @@ def test_dump(monkeypatch):
assert mystdout.getvalue() == "61 62 63 \n" assert mystdout.getvalue() == "61 62 63 \n"
def test_pad_deprecation(): def test_pad_deprecation() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert IptcImagePlugin.PAD == b"\0\0\0\0" assert IptcImagePlugin.PAD == b"\0\0\0\0"

View File

@ -1,8 +1,12 @@
from __future__ import annotations from __future__ import annotations
import os import os
import re import re
import warnings import warnings
from io import BytesIO from io import BytesIO
from pathlib import Path
from types import ModuleType
from typing import Any
import pytest import pytest
@ -30,6 +34,7 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
ElementTree: ModuleType | None
try: try:
from defusedxml import ElementTree from defusedxml import ElementTree
except ImportError: except ImportError:
@ -40,7 +45,7 @@ TEST_FILE = "Tests/images/hopper.jpg"
@skip_unless_feature("jpg") @skip_unless_feature("jpg")
class TestFileJpeg: class TestFileJpeg:
def roundtrip(self, im, **options): def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO() out = BytesIO()
im.save(out, "JPEG", **options) im.save(out, "JPEG", **options)
test_bytes = out.tell() test_bytes = out.tell()
@ -49,7 +54,7 @@ class TestFileJpeg:
im.bytes = test_bytes # for testing only im.bytes = test_bytes # for testing only
return im 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 """Generates a very hard to compress file
:param size: tuple :param size: tuple
:param mode: optional image mode :param mode: optional image mode
@ -57,7 +62,7 @@ class TestFileJpeg:
""" """
return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) 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 # internal version number
assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) assert re.search(r"\d+\.\d+$", features.version_codec("jpg"))
@ -69,13 +74,13 @@ class TestFileJpeg:
assert im.get_format_mimetype() == "image/jpeg" assert im.get_format_mimetype() == "image/jpeg"
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero(self, size, tmp_path): def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
im = Image.new("RGB", size) im = Image.new("RGB", size)
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(f) im.save(f)
def test_app(self): def test_app(self) -> None:
# Test APP/COM reader (@PIL135) # Test APP/COM reader (@PIL135)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00")
@ -88,7 +93,7 @@ class TestFileJpeg:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
assert im.app["COM"] == im.info["comment"] 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: with Image.open(TEST_FILE) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" 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 assert "comment" not in reloaded.info
# Test that a comment argument overrides the default comment # 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() out = BytesIO()
im.save(out, format="JPEG", comment=comment) im.save(out, format="JPEG", comment=comment)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
if not isinstance(comment, bytes): assert reloaded.info["comment"] == b"Test comment text"
comment = comment.encode()
assert reloaded.info["comment"] == comment
def test_cmyk(self): def test_cmyk(self) -> None:
# Test CMYK handling. Thanks to Tim and Charlie for test data, # Test CMYK handling. Thanks to Tim and Charlie for test data,
# Michael for getting me to look one more time. # Michael for getting me to look one more time.
f = "Tests/images/pil_sample_cmyk.jpg" f = "Tests/images/pil_sample_cmyk.jpg"
@ -142,8 +145,8 @@ class TestFileJpeg:
) )
assert k > 0.9 assert k > 0.9
def test_rgb(self): def test_rgb(self) -> None:
def getchannels(im): def getchannels(im: Image.Image) -> tuple[int, int, int]:
return tuple(v[0] for v in im.layer) return tuple(v[0] for v in im.layer)
im = hopper() im = hopper()
@ -159,8 +162,8 @@ class TestFileJpeg:
"test_image_path", "test_image_path",
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
) )
def test_dpi(self, test_image_path): def test_dpi(self, test_image_path: str) -> None:
def test(xdpi, ydpi=None): def test(xdpi: int, ydpi: int | None = None):
with Image.open(test_image_path) as im: with Image.open(test_image_path) as im:
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
return im.info.get("dpi") return im.info.get("dpi")
@ -173,7 +176,7 @@ class TestFileJpeg:
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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 # Test ICC support
with Image.open("Tests/images/rgb.jpg") as im1: with Image.open("Tests/images/rgb.jpg") as im1:
icc_profile = im1.info["icc_profile"] icc_profile = im1.info["icc_profile"]
@ -205,7 +208,7 @@ class TestFileJpeg:
ImageFile.MAXBLOCK * 4 + 3, # large block 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 # Make sure that the "extra" support handles large blocks
# The ICC APP marker can store 65519 bytes per marker, so # The ICC APP marker can store 65519 bytes per marker, so
# using a 4-byte test code should allow us to detect out of # using a 4-byte test code should allow us to detect out of
@ -218,7 +221,7 @@ class TestFileJpeg:
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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 # https://github.com/python-pillow/Pillow/issues/148
# Sometimes the meta data on the icc_profile block is bigger than # Sometimes the meta data on the icc_profile block is bigger than
# Image.MAXBLOCK or the image size. # Image.MAXBLOCK or the image size.
@ -242,7 +245,7 @@ class TestFileJpeg:
f = str(tmp_path / "temp3.jpg") f = str(tmp_path / "temp3.jpg")
im.save(f, progressive=True, quality=94, exif=b" " * 43668) im.save(f, progressive=True, quality=94, exif=b" " * 43668)
def test_optimize(self): def test_optimize(self) -> None:
im1 = self.roundtrip(hopper()) im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), optimize=0) im2 = self.roundtrip(hopper(), optimize=0)
im3 = self.roundtrip(hopper(), optimize=1) im3 = self.roundtrip(hopper(), optimize=1)
@ -251,14 +254,14 @@ class TestFileJpeg:
assert im1.bytes >= im2.bytes assert im1.bytes >= im2.bytes
assert im1.bytes >= im3.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 # https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
# this requires ~ 1.5x Image.MAXBLOCK # this requires ~ 1.5x Image.MAXBLOCK
im = Image.new("RGB", (4096, 4096), 0xFF3333) im = Image.new("RGB", (4096, 4096), 0xFF3333)
im.save(f, format="JPEG", optimize=True) im.save(f, format="JPEG", optimize=True)
def test_progressive(self): def test_progressive(self) -> None:
im1 = self.roundtrip(hopper()) im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), progressive=False) im2 = self.roundtrip(hopper(), progressive=False)
im3 = self.roundtrip(hopper(), progressive=True) im3 = self.roundtrip(hopper(), progressive=True)
@ -269,25 +272,25 @@ class TestFileJpeg:
assert_image_equal(im1, im3) assert_image_equal(im1, im3)
assert im1.bytes >= im3.bytes 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") f = str(tmp_path / "temp.jpg")
# this requires ~ 1.5x Image.MAXBLOCK # this requires ~ 1.5x Image.MAXBLOCK
im = Image.new("RGB", (4096, 4096), 0xFF3333) im = Image.new("RGB", (4096, 4096), 0xFF3333)
im.save(f, format="JPEG", progressive=True) im.save(f, format="JPEG", progressive=True)
def test_progressive_large_buffer_highest_quality(self, tmp_path): def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
im = self.gen_random_image((255, 255)) im = self.gen_random_image((255, 255))
# this requires more bytes than pixels in the image # this requires more bytes than pixels in the image
im.save(f, format="JPEG", progressive=True, quality=100) im.save(f, format="JPEG", progressive=True, quality=100)
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. # Issue 2272, quality 90 cmyk image is tripping the large buffer bug.
f = BytesIO() f = BytesIO()
im = self.gen_random_image((256, 256), "CMYK") im = self.gen_random_image((256, 256), "CMYK")
im.save(f, format="JPEG", progressive=True, quality=94) 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 # https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
im = hopper() im = hopper()
@ -296,12 +299,12 @@ class TestFileJpeg:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(f, "JPEG", quality=90, exif=b"1" * 65534) 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: with Image.open("Tests/images/exif_typeerror.jpg") as im:
# Should not raise a TypeError # Should not raise a TypeError
im._getexif() im._getexif()
def test_exif_gps(self, tmp_path): def test_exif_gps(self, tmp_path: Path) -> None:
expected_exif_gps = { expected_exif_gps = {
0: b"\x00\x00\x00\x01", 0: b"\x00\x00\x00\x01",
2: 4294967295, 2: 4294967295,
@ -326,7 +329,7 @@ class TestFileJpeg:
exif = reloaded._getexif() exif = reloaded._getexif()
assert exif[gps_index] == expected_exif_gps 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: with Image.open("Tests/images/empty_gps_ifd.jpg") as im:
exif = im.getexif() exif = im.getexif()
del exif[0x8769] del exif[0x8769]
@ -344,7 +347,7 @@ class TestFileJpeg:
# Assert that it was transposed # Assert that it was transposed
assert 0x0112 not in exif 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 # In 7.2.0, Exif rationals were changed to be read as
# TiffImagePlugin.IFDRational. This class had a bug in __eq__, # TiffImagePlugin.IFDRational. This class had a bug in __eq__,
# breaking the self-equality of Exif data # breaking the self-equality of Exif data
@ -354,7 +357,7 @@ class TestFileJpeg:
exifs.append(im._getexif()) exifs.append(im._getexif())
assert exifs[0] == exifs[1] 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. # 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 # 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 # this test passes on 2.9 and 3.1, but not 3.0
@ -389,12 +392,12 @@ class TestFileJpeg:
for tag, value in expected_exif.items(): for tag, value in expected_exif.items():
assert value == exif[tag] 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: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im:
# Should not raise a TypeError # Should not raise a TypeError
im._getexif() im._getexif()
def test_progressive_compat(self): def test_progressive_compat(self) -> None:
im1 = self.roundtrip(hopper()) im1 = self.roundtrip(hopper())
assert not im1.info.get("progressive") assert not im1.info.get("progressive")
assert not im1.info.get("progression") assert not im1.info.get("progression")
@ -415,7 +418,7 @@ class TestFileJpeg:
assert im3.info.get("progressive") assert im3.info.get("progressive")
assert im3.info.get("progression") assert im3.info.get("progression")
def test_quality(self): def test_quality(self) -> None:
im1 = self.roundtrip(hopper()) im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), quality=50) im2 = self.roundtrip(hopper(), quality=50)
assert_image(im1, im2.mode, im2.size) assert_image(im1, im2.mode, im2.size)
@ -425,13 +428,13 @@ class TestFileJpeg:
assert_image(im1, im3.mode, im3.size) assert_image(im1, im3.mode, im3.size)
assert im2.bytes > im3.bytes assert im2.bytes > im3.bytes
def test_smooth(self): def test_smooth(self) -> None:
im1 = self.roundtrip(hopper()) im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), smooth=100) im2 = self.roundtrip(hopper(), smooth=100)
assert_image(im1, im2.mode, im2.size) assert_image(im1, im2.mode, im2.size)
def test_subsampling(self): def test_subsampling(self) -> None:
def getsampling(im): def getsampling(im: Image.Image):
layer = im.layer layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
@ -439,46 +442,46 @@ class TestFileJpeg:
for subsampling in (-1, 3): # (default, invalid) for subsampling in (-1, 3): # (default, invalid)
im = self.roundtrip(hopper(), subsampling=subsampling) im = self.roundtrip(hopper(), subsampling=subsampling)
assert getsampling(im) == (2, 2, 1, 1, 1, 1) assert getsampling(im) == (2, 2, 1, 1, 1, 1)
for subsampling in (0, "4:4:4"): for subsampling1 in (0, "4:4:4"):
im = self.roundtrip(hopper(), subsampling=subsampling) im = self.roundtrip(hopper(), subsampling=subsampling1)
assert getsampling(im) == (1, 1, 1, 1, 1, 1) assert getsampling(im) == (1, 1, 1, 1, 1, 1)
for subsampling in (1, "4:2:2"): for subsampling1 in (1, "4:2:2"):
im = self.roundtrip(hopper(), subsampling=subsampling) im = self.roundtrip(hopper(), subsampling=subsampling1)
assert getsampling(im) == (2, 1, 1, 1, 1, 1) assert getsampling(im) == (2, 1, 1, 1, 1, 1)
for subsampling in (2, "4:2:0", "4:1:1"): for subsampling1 in (2, "4:2:0", "4:1:1"):
im = self.roundtrip(hopper(), subsampling=subsampling) im = self.roundtrip(hopper(), subsampling=subsampling1)
assert getsampling(im) == (2, 2, 1, 1, 1, 1) assert getsampling(im) == (2, 2, 1, 1, 1, 1)
# RGB colorspace # RGB colorspace
for subsampling in (-1, 0, "4:4:4"): for subsampling1 in (-1, 0, "4:4:4"):
# "4:4:4" doesn't really make sense for RGB, but the conversion # "4:4:4" doesn't really make sense for RGB, but the conversion
# to an integer happens at a higher level # to an integer happens at a higher level
im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1)
assert getsampling(im) == (1, 1, 1, 1, 1, 1) assert getsampling(im) == (1, 1, 1, 1, 1, 1)
for subsampling in (1, "4:2:2", 2, "4:2:0", 3): for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3):
with pytest.raises(OSError): with pytest.raises(OSError):
self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling) self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1)
with pytest.raises(TypeError): with pytest.raises(TypeError):
self.roundtrip(hopper(), subsampling="1:1:1") 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: with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
info = im._getexif() info = im._getexif()
assert info[305] == "Adobe Photoshop CS Macintosh" 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: with Image.open("Tests/images/flower.jpg") as im:
ims = im.get_child_images() ims = im.get_child_images()
assert len(ims) == 1 assert len(ims) == 1
assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.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: with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im._getmp() is None assert im._getmp() is None
def test_quality_keep(self, tmp_path): def test_quality_keep(self, tmp_path: Path) -> None:
# RGB # RGB
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
@ -492,13 +495,13 @@ class TestFileJpeg:
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
im.save(f, quality="keep") 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 # https://github.com/python-pillow/Pillow/issues/630
filename = "Tests/images/junk_jpeg_header.jpg" filename = "Tests/images/junk_jpeg_header.jpg"
with Image.open(filename): with Image.open(filename):
pass pass
def test_ff00_jpeg_header(self): def test_ff00_jpeg_header(self) -> None:
filename = "Tests/images/jpeg_ff00_header.jpg" filename = "Tests/images/jpeg_ff00_header.jpg"
with Image.open(filename): with Image.open(filename):
pass pass
@ -506,7 +509,7 @@ class TestFileJpeg:
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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" filename = "Tests/images/truncated_jpeg.jpg"
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
with Image.open(filename) as im: with Image.open(filename) as im:
@ -514,7 +517,7 @@ class TestFileJpeg:
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
assert im.getbbox() is not None 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" filename = "Tests/images/truncated_jpeg.jpg"
with Image.open(filename) as im: with Image.open(filename) as im:
with pytest.raises(OSError): with pytest.raises(OSError):
@ -527,8 +530,8 @@ class TestFileJpeg:
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )
def test_qtables(self, tmp_path): def test_qtables(self, tmp_path: Path) -> None:
def _n_qtables_helper(n, test_file): def _n_qtables_helper(n: int, test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
im.save(f, qtables=[[n] * 64] * n) im.save(f, qtables=[[n] * 64] * n)
@ -636,24 +639,24 @@ class TestFileJpeg:
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.roundtrip(im, qtables=[[1, 2, 3, 4]]) 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: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
assert len(im.quantization) == 2 assert len(im.quantization) == 2
assert len(im.quantization[0]) == 64 assert len(im.quantization[0]) == 64
assert max(im.quantization[0]) > 255 assert max(im.quantization[0]) > 255
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: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
im2 = self.roundtrip(im, qtables="keep") im2 = self.roundtrip(im, qtables="keep")
assert im.quantization == im2.quantization 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: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) im2 = self.roundtrip(im, qtables={0: im.quantization[0]})
assert len(im2.quantization) == 1 assert len(im2.quantization) == 1
assert im2.quantization[0] == im.quantization[0] 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: with Image.open(TEST_FILE) as im:
im2 = self.roundtrip(im, quality=10) im2 = self.roundtrip(im, quality=10)
assert len(im2.quantization) == 2 assert len(im2.quantization) == 2
@ -664,7 +667,7 @@ class TestFileJpeg:
"blocks, rows, markers", "blocks, rows, markers",
((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), ((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 im = Image.new("RGB", (32, 32)) # 16 MCUs
out = BytesIO() out = BytesIO()
im.save( im.save(
@ -678,20 +681,20 @@ class TestFileJpeg:
assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg(self): def test_load_djpeg(self) -> None:
with Image.open(TEST_FILE) as img: with Image.open(TEST_FILE) as img:
img.load_djpeg() img.load_djpeg()
assert_image_similar_tofile(img, TEST_FILE, 5) assert_image_similar_tofile(img, TEST_FILE, 5)
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") @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: with Image.open(TEST_FILE) as img:
tempfile = str(tmp_path / "temp.jpg") tempfile = str(tmp_path / "temp.jpg")
JpegImagePlugin._save_cjpeg(img, 0, tempfile) JpegImagePlugin._save_cjpeg(img, 0, tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright # Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17) assert_image_similar_tofile(img, tempfile, 17)
def test_no_duplicate_0x1001_tag(self): def test_no_duplicate_0x1001_tag(self) -> None:
# Arrange # Arrange
tag_ids = {v: k for k, v in ExifTags.TAGS.items()} tag_ids = {v: k for k, v in ExifTags.TAGS.items()}
@ -699,7 +702,7 @@ class TestFileJpeg:
assert tag_ids["RelatedImageWidth"] == 0x1001 assert tag_ids["RelatedImageWidth"] == 0x1001
assert tag_ids["RelatedImageLength"] == 0x1002 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)) im = self.gen_random_image((512, 512))
f = str(tmp_path / "temp.jpeg") f = str(tmp_path / "temp.jpeg")
im.save(f, quality=100, optimize=True) im.save(f, quality=100, optimize=True)
@ -710,7 +713,7 @@ class TestFileJpeg:
reloaded.save(f, quality="keep", progressive=True) reloaded.save(f, quality="keep", progressive=True)
reloaded.save(f, quality="keep", optimize=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""" """Treat unknown MPO as JPEG"""
# Arrange # Arrange
@ -722,20 +725,20 @@ class TestFileJpeg:
assert im.format == "JPEG" assert im.format == "JPEG"
@pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) @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() out = BytesIO()
img = Image.new(mode, (20, 20)) img = Image.new(mode, (20, 20))
img.save(out, "JPEG") img.save(out, "JPEG")
@pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) @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 # ref https://github.com/python-pillow/Pillow/issues/2005
out = BytesIO() out = BytesIO()
img = Image.new(mode, (20, 20)) img = Image.new(mode, (20, 20))
with pytest.raises(OSError): with pytest.raises(OSError):
img.save(out, "JPEG") 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 # Arrange
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
@ -747,7 +750,7 @@ class TestFileJpeg:
reloaded.load() reloaded.load()
assert im.info["dpi"] == reloaded.info["dpi"] 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") outfile = str(tmp_path / "temp.jpg")
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
im.save(outfile, dpi=(72.2, 72.2)) im.save(outfile, dpi=(72.2, 72.2))
@ -760,7 +763,7 @@ class TestFileJpeg:
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert reloaded.info["dpi"] == (73, 73) assert reloaded.info["dpi"] == (73, 73)
def test_dpi_tuple_from_exif(self): def test_dpi_tuple_from_exif(self) -> None:
# Arrange # Arrange
# This Photoshop CC 2017 image has DPI in EXIF not metadata # This Photoshop CC 2017 image has DPI in EXIF not metadata
# EXIF XResolution is (2000000, 10000) # EXIF XResolution is (2000000, 10000)
@ -768,7 +771,7 @@ class TestFileJpeg:
# Act / Assert # Act / Assert
assert im.info.get("dpi") == (200, 200) assert im.info.get("dpi") == (200, 200)
def test_dpi_int_from_exif(self): def test_dpi_int_from_exif(self) -> None:
# Arrange # Arrange
# This image has DPI in EXIF not metadata # This image has DPI in EXIF not metadata
# EXIF XResolution is 72 # EXIF XResolution is 72
@ -776,7 +779,7 @@ class TestFileJpeg:
# Act / Assert # Act / Assert
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_dpi_from_dpcm_exif(self): def test_dpi_from_dpcm_exif(self) -> None:
# Arrange # Arrange
# This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm:
# exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg
@ -784,7 +787,7 @@ class TestFileJpeg:
# Act / Assert # Act / Assert
assert im.info.get("dpi") == (508, 508) assert im.info.get("dpi") == (508, 508)
def test_dpi_exif_zero_division(self): def test_dpi_exif_zero_division(self) -> None:
# Arrange # Arrange
# This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0:
# exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg
@ -793,7 +796,7 @@ class TestFileJpeg:
# This should return the default, and not raise a ZeroDivisionError # This should return the default, and not raise a ZeroDivisionError
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_dpi_exif_string(self): def test_dpi_exif_string(self) -> None:
# Arrange # Arrange
# 0x011A tag in this exif contains string '300300\x02' # 0x011A tag in this exif contains string '300300\x02'
with Image.open("Tests/images/broken_exif_dpi.jpg") as im: with Image.open("Tests/images/broken_exif_dpi.jpg") as im:
@ -801,14 +804,14 @@ class TestFileJpeg:
# This should return the default # This should return the default
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_dpi_exif_truncated(self): def test_dpi_exif_truncated(self) -> None:
# Arrange # Arrange
with Image.open("Tests/images/truncated_exif_dpi.jpg") as im: with Image.open("Tests/images/truncated_exif_dpi.jpg") as im:
# Act / Assert # Act / Assert
# This should return the default # This should return the default
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_no_dpi_in_exif(self): def test_no_dpi_in_exif(self) -> None:
# Arrange # Arrange
# This is photoshop-200dpi.jpg with resolution removed from EXIF: # This is photoshop-200dpi.jpg with resolution removed from EXIF:
# exiftool "-*resolution*"= photoshop-200dpi.jpg # exiftool "-*resolution*"= photoshop-200dpi.jpg
@ -818,7 +821,7 @@ class TestFileJpeg:
# https://exiv2.org/tags.html # https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72) 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 # This is no-dpi-in-exif with the tiff header of the exif block
# hexedited from MM * to FF FF FF FF # hexedited from MM * to FF FF FF FF
with Image.open("Tests/images/invalid-exif.jpg") as im: with Image.open("Tests/images/invalid-exif.jpg") as im:
@ -829,7 +832,7 @@ class TestFileJpeg:
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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: with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif() exif = im.getexif()
assert exif[282] == 180 assert exif[282] == 180
@ -841,14 +844,14 @@ class TestFileJpeg:
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.getexif()[282] == 180 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 # When no x or y resolution is defined in EXIF
with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im:
# This should return the default, and not a ValueError or # This should return the default, and not a ValueError or
# OSError for an unidentified image. # OSError for an unidentified image.
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_ifd_offset_exif(self): def test_ifd_offset_exif(self) -> None:
# Arrange # Arrange
# This image has been manually hexedited to have an IFD offset of 10, # This image has been manually hexedited to have an IFD offset of 10,
# in contrast to normal 8 # in contrast to normal 8
@ -856,14 +859,14 @@ class TestFileJpeg:
# Act / Assert # Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09" assert im._getexif()[306] == "2017:03:13 23:03:09"
def test_multiple_exif(self): def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im: with Image.open("Tests/images/multiple_exif.jpg") as im:
assert im.info["exif"] == b"Exif\x00\x00firstsecond" assert im.info["exif"] == b"Exif\x00\x00firstsecond"
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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: with Image.open("Tests/images/photoshop-200dpi.jpg") as im:
assert im.info["photoshop"][0x03ED] == { assert im.info["photoshop"][0x03ED] == {
"XResolution": 200.0, "XResolution": 200.0,
@ -880,14 +883,14 @@ class TestFileJpeg:
with Image.open("Tests/images/app13.jpg") as im: with Image.open("Tests/images/app13.jpg") as im:
assert "photoshop" not in im.info 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: with Image.open("Tests/images/app13-multiple.jpg") as im:
assert "photoshop" in im.info assert "photoshop" in im.info
assert 24 == len(im.info["photoshop"]) assert 24 == len(im.info["photoshop"])
apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"]
assert [65504, 24] == apps_13_lengths 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: with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im.info["adobe_transform"] == 1 assert im.info["adobe_transform"] == 1
@ -901,11 +904,11 @@ class TestFileJpeg:
assert "adobe" in im.info assert "adobe" in im.info
assert "adobe_transform" not 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: with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile" assert im.info["icc_profile"] == b"profile"
def test_jpeg_magic_number(self): def test_jpeg_magic_number(self) -> None:
size = 4097 size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer.max_pos = 0 buffer.max_pos = 0
@ -924,7 +927,7 @@ class TestFileJpeg:
# Assert the entire file has not been read # Assert the entire file has not been read
assert 0 < buffer.max_pos < size 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: with Image.open("Tests/images/xmp_test.jpg") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns( with pytest.warns(
@ -953,7 +956,7 @@ class TestFileJpeg:
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
assert im.getxmp() == {} 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: with Image.open("Tests/images/xmp_no_prefix.jpg") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns( with pytest.warns(
@ -964,7 +967,7 @@ class TestFileJpeg:
else: else:
assert im.getxmp() == {"xmpmeta": {"key": "value"}} 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: with Image.open("Tests/images/xmp_padded.jpg") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns( with pytest.warns(
@ -976,16 +979,16 @@ class TestFileJpeg:
assert im.getxmp() == {"xmpmeta": None} assert im.getxmp() == {"xmpmeta": None}
@pytest.mark.timeout(timeout=1) @pytest.mark.timeout(timeout=1)
def test_eof(self): def test_eof(self) -> None:
# Even though this decoder never says that it is finished # Even though this decoder never says that it is finished
# the image should still end when there is no new data # the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder): class InfiniteMockPyDecoder(ImageFile.PyDecoder):
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
return 0, 0 return 0, 0
decoder = InfiniteMockPyDecoder(None) decoder = InfiniteMockPyDecoder(None)
def closure(mode, *args): def closure(mode: str, *args) -> InfiniteMockPyDecoder:
decoder.__init__(mode, *args) decoder.__init__(mode, *args)
return decoder return decoder
@ -999,7 +1002,7 @@ class TestFileJpeg:
im.load() im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_separate_tables(self): def test_separate_tables(self) -> None:
im = hopper() im = hopper()
data = [] # [interchange, tables-only, image-only] data = [] # [interchange, tables-only, image-only]
for streamtype in range(3): for streamtype in range(3):
@ -1021,14 +1024,14 @@ class TestFileJpeg:
with Image.open(BytesIO(data[1] + data[2])) as combined_im: with Image.open(BytesIO(data[1] + data[2])) as combined_im:
assert_image_equal(interchange_im, combined_im) assert_image_equal(interchange_im, combined_im)
def test_repr_jpeg(self): def test_repr_jpeg(self) -> None:
im = hopper() im = hopper()
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
assert repr_jpeg.format == "JPEG" assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17) 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") im = hopper("F")
assert im._repr_jpeg_() is None assert im._repr_jpeg_() is None
@ -1037,7 +1040,7 @@ class TestFileJpeg:
@pytest.mark.skipif(not is_win32(), reason="Windows only") @pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg") @skip_unless_feature("jpg")
class TestFileCloseW32: class TestFileCloseW32:
def test_fd_leak(self, tmp_path): def test_fd_leak(self, tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.jpg") tmpfile = str(tmp_path / "temp.jpg")
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:

View File

@ -1,7 +1,10 @@
from __future__ import annotations from __future__ import annotations
import os import os
import re import re
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import Any
import pytest import pytest
@ -34,7 +37,7 @@ test_card.load()
# 'Not enough memory to handle tile data' # 'Not enough memory to handle tile data'
def roundtrip(im, **options): def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO() out = BytesIO()
im.save(out, "JPEG2000", **options) im.save(out, "JPEG2000", **options)
test_bytes = out.tell() test_bytes = out.tell()
@ -45,7 +48,7 @@ def roundtrip(im, **options):
return im return im
def test_sanity(): def test_sanity() -> None:
# Internal version number # Internal version number
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) 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" assert im.get_format_mimetype() == "image/jp2"
def test_jpf(): def test_jpf() -> None:
with Image.open("Tests/images/balloon.jpf") as im: with Image.open("Tests/images/balloon.jpf") as im:
assert im.format == "JPEG2000" assert im.format == "JPEG2000"
assert im.get_format_mimetype() == "image/jpx" assert im.get_format_mimetype() == "image/jpx"
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file)
def test_bytesio(): def test_bytesio() -> None:
with open("Tests/images/test-card-lossless.jp2", "rb") as f: with open("Tests/images/test-card-lossless.jp2", "rb") as f:
data = BytesIO(f.read()) data = BytesIO(f.read())
assert_image_similar_tofile(test_card, data, 1.0e-3) assert_image_similar_tofile(test_card, data, 1.0e-3)
@ -81,7 +84,7 @@ def test_bytesio():
# PIL (they were made using Adobe Photoshop) # 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: with Image.open("Tests/images/test-card-lossless.jp2") as im:
im.load() im.load()
outfile = str(tmp_path / "temp_test-card.png") 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) assert_image_similar(im, test_card, 1.0e-3)
def test_lossy_tiled(): def test_lossy_tiled() -> None:
assert_image_similar_tofile( assert_image_similar_tofile(
test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0
) )
def test_lossless_rt(): def test_lossless_rt() -> None:
im = roundtrip(test_card) im = roundtrip(test_card)
assert_image_equal(im, test_card) assert_image_equal(im, test_card)
def test_lossy_rt(): def test_lossy_rt() -> None:
im = roundtrip(test_card, quality_layers=[20]) im = roundtrip(test_card, quality_layers=[20])
assert_image_similar(im, test_card, 2.0) 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)) im = roundtrip(test_card, tile_size=(128, 128))
assert_image_equal(im, test_card) 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)) im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32))
assert_image_equal(im, test_card) assert_image_equal(im, test_card)
def test_tiled_offset_too_small(): def test_tiled_offset_too_small() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) 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]) im = roundtrip(test_card, irreversible=True, quality_layers=[20])
assert_image_similar(im, test_card, 2.0) 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") im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP")
assert_image_similar(im, test_card, 2.0) 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") im = roundtrip(test_card, num_resolutions=8, progression="RLCP")
assert_image_equal(im, test_card) assert_image_equal(im, test_card)
@pytest.mark.parametrize("num_resolutions", range(2, 6)) @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) d = 1 << (num_resolutions - 1)
im = test_card.resize((d - 1, d - 1)) im = test_card.resize((d - 1, d - 1))
with pytest.raises(OSError): with pytest.raises(OSError):
@ -145,7 +148,7 @@ def test_default_num_resolutions(num_resolutions):
assert_image_equal(im, reloaded) assert_image_equal(im, reloaded)
def test_reduce(): def test_reduce() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im: with Image.open("Tests/images/test-card-lossless.jp2") as im:
assert callable(im.reduce) assert callable(im.reduce)
@ -159,7 +162,7 @@ def test_reduce():
assert im.size == (40, 30) 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: with Image.open("Tests/images/test-card-lossless.jp2") as im:
assert im.info["dpi"] == (71.9836, 71.9836) assert im.info["dpi"] == (71.9836, 71.9836)
@ -167,7 +170,7 @@ def test_load_dpi():
assert "dpi" not in im.info assert "dpi" not in im.info
def test_restricted_icc_profile(): def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
try: try:
# JPEG2000 image with a restricted ICC profile and a known colorspace # 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 ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_header_errors(): def test_header_errors() -> None:
for path in ( for path in (
"Tests/images/invalid_header_length.jp2", "Tests/images/invalid_header_length.jp2",
"Tests/images/not_enough_data.jp2", "Tests/images/not_enough_data.jp2",
@ -191,17 +194,17 @@ def test_header_errors():
pass pass
def test_layers_type(tmp_path): def test_layers_type(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp_layers.jp2") outfile = str(tmp_path / "temp_layers.jp2")
for quality_layers in [[100, 50, 10], (100, 50, 10), None]: for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
test_card.save(outfile, quality_layers=quality_layers) 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): 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() out = BytesIO()
test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP")
out.seek(0) out.seek(0)
@ -231,7 +234,7 @@ def test_layers():
("foo.jp2", {"no_jp2": False}, 4, b"jP"), ("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() out = BytesIO()
if name: if name:
out.name = name out.name = name
@ -240,7 +243,7 @@ def test_no_jp2(name, args, offset, data):
assert out.read(2) == data assert out.read(2) == data
def test_mct(): def test_mct() -> None:
# Three component # Three component
for val in (0, 1): for val in (0, 1):
out = BytesIO() out = BytesIO()
@ -261,7 +264,7 @@ def test_mct():
assert_image_similar(im, jp2, 1.0e-3) 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") outfile = str(tmp_path / "temp.jp2")
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
@ -276,7 +279,7 @@ def test_sgnd(tmp_path):
@pytest.mark.parametrize("ext", (".j2k", ".jp2")) @pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_rgba(ext): def test_rgba(ext: str) -> None:
# Arrange # Arrange
with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
# Act # Act
@ -287,47 +290,47 @@ def test_rgba(ext):
@pytest.mark.parametrize("ext", (".j2k", ".jp2")) @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: with Image.open("Tests/images/16bit.cropped" + ext) as im:
im.load() im.load()
assert im.mode == "I;16" 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: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit:
assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) 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: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit:
assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) 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: with Image.open("Tests/images/16bit.cropped.j2k") as j2k:
im = roundtrip(j2k) im = roundtrip(j2k)
assert_image_equal(im, 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: with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
im = roundtrip(jp2) im = roundtrip(jp2)
assert_image_equal(im, 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: with Image.open("Tests/images/issue_6194.j2k") as im:
assert im.getpixel((5, 5)) == 31 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. # prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open("Tests/images/unbound_variable.jp2"): with Image.open("Tests/images/unbound_variable.jp2"):
pass pass
def test_parser_feed(): def test_parser_feed() -> None:
# Arrange # Arrange
with open("Tests/images/test-card-lossless.jp2", "rb") as f: with open("Tests/images/test-card-lossless.jp2", "rb") as f:
data = f.read() data = f.read()
@ -344,12 +347,12 @@ def test_parser_feed():
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
) )
@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) @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" test = f"{EXTRA_DIR}/{name}.jp2"
reference = f"{EXTRA_DIR}/{name}.ppm" reference = f"{EXTRA_DIR}/{name}.ppm"
with Image.open(test) as im: with Image.open(test) as im:
epsilon = 3 # for YCbCr images epsilon = 3.0 # for YCbCr images
with Image.open(reference) as im2: with Image.open(reference) as im2:
width, height = im2.size width, height = im2.size
if name[-1] == "2": if name[-1] == "2":
@ -360,7 +363,7 @@ def test_subsampling_decode(name):
assert_image_similar(im, expected, epsilon) assert_image_similar(im, expected, epsilon)
def test_comment(): def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im: with Image.open("Tests/images/comment.jp2") as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
@ -371,7 +374,7 @@ def test_comment():
pass pass
def test_save_comment(): def test_save_comment() -> None:
for comment in ("Created by Pillow", b"Created by Pillow"): for comment in ("Created by Pillow", b"Created by Pillow"):
out = BytesIO() out = BytesIO()
test_card.save(out, "JPEG2000", comment=comment) test_card.save(out, "JPEG2000", comment=comment)
@ -398,7 +401,7 @@ def test_save_comment():
"Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", "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 open(test_file, "rb") as f:
with Image.open(f) as im: with Image.open(f) as im:
# Valgrind should not complain here # Valgrind should not complain here
@ -409,7 +412,7 @@ def test_crashes(test_file):
@skip_unless_feature_version("jpg_2000", "2.4.0") @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 # Search the start of the codesteam for PLT
out = BytesIO() out = BytesIO()
test_card.save(out, "JPEG2000", no_jp2=True, plt=True) test_card.save(out, "JPEG2000", no_jp2=True, plt=True)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import base64 import base64
import io import io
import itertools import itertools
@ -6,6 +7,7 @@ import os
import re import re
import sys import sys
from collections import namedtuple from collections import namedtuple
from pathlib import Path
import pytest import pytest
@ -25,7 +27,7 @@ from .helper import (
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
class LibTiffTestCase: 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""" """Helper tests that assert basic sanity about the g4 tiff reading"""
# 1 bit # 1 bit
assert im.mode == "1" assert im.mode == "1"
@ -49,10 +51,10 @@ class LibTiffTestCase:
class TestFileLibTiff(LibTiffTestCase): class TestFileLibTiff(LibTiffTestCase):
def test_version(self): def test_version(self) -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) 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 the ordinary file path load path"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
@ -60,12 +62,12 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (500, 500) assert im.size == (500, 500)
self._assert_noerr(tmp_path, im) 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" test_file = "Tests/images/pport_g4.tif"
with Image.open(test_file) as im: with Image.open(test_file) as im:
self._assert_noerr(tmp_path, 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""" """Testing the string load path"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
@ -74,7 +76,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (500, 500) assert im.size == (500, 500)
self._assert_noerr(tmp_path, im) 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""" """Testing the stringio loading code path"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO() s = io.BytesIO()
@ -85,7 +87,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (500, 500) assert im.size == (500, 500)
self._assert_noerr(tmp_path, im) 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""" """Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO() s = io.BytesIO()
@ -97,18 +99,18 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (500, 500) assert im.size == (500, 500)
self._assert_noerr(tmp_path, im) 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""" """Checking that we're actually getting the data that we expect"""
with Image.open("Tests/images/hopper_bw_500.png") as png: with Image.open("Tests/images/hopper_bw_500.png") as png:
assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif") assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif")
# see https://github.com/python-pillow/Pillow/issues/279 # 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""" """Checking that we're actually getting the data that we expect"""
with Image.open("Tests/images/g4-fillorder-test.tif") as g4: with Image.open("Tests/images/g4-fillorder-test.tif") as g4:
assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png") 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""" """Checking to see that the saved image is the same as what we wrote"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig: with Image.open(test_file) as orig:
@ -127,7 +129,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert orig.tobytes() != reread.tobytes() 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" test_file = "Tests/images/tiff_adobe_deflate.tif"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.mode == "RGB" assert im.mode == "RGB"
@ -138,7 +140,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
@pytest.mark.parametrize("legacy_api", (False, True)) @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""" """Test metadata writing through libtiff"""
f = str(tmp_path / "temp.tiff") f = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper_g4.tif") as img: 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" assert field in reloaded, f"{field} not in metadata"
@pytest.mark.valgrind_known_error(reason="Known invalid 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 # 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 # any sense, so we're running up against limits where we're asking
# libtiff to do stupid things. # libtiff to do stupid things.
@ -240,8 +242,8 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.WRITE_LIBTIFF = False
def test_custom_metadata(self, tmp_path): def test_custom_metadata(self, tmp_path: Path) -> None:
tc = namedtuple("test_case", "value,type,supported_by_default") tc = namedtuple("tc", "value,type,supported_by_default")
custom = { custom = {
37000 + k: v 37000 + k: v
for k, v in enumerate( for k, v in enumerate(
@ -282,7 +284,9 @@ class TestFileLibTiff(LibTiffTestCase):
for libtiff in libtiffs: for libtiff in libtiffs:
TiffImagePlugin.WRITE_LIBTIFF = libtiff TiffImagePlugin.WRITE_LIBTIFF = libtiff
def check_tags(tiffinfo): def check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
) -> None:
im = hopper() im = hopper()
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -321,7 +325,7 @@ class TestFileLibTiff(LibTiffTestCase):
) )
TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.WRITE_LIBTIFF = False
def test_subifd(self, tmp_path): def test_subifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/g4_orientation_6.tif") as im: with Image.open("Tests/images/g4_orientation_6.tif") as im:
im.tag_v2[SUBIFD] = 10000 im.tag_v2[SUBIFD] = 10000
@ -329,7 +333,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault # Should not segfault
im.save(outfile) im.save(outfile)
def test_xmlpacket_tag(self, tmp_path): def test_xmlpacket_tag(self, tmp_path: Path) -> None:
TiffImagePlugin.WRITE_LIBTIFF = True TiffImagePlugin.WRITE_LIBTIFF = True
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -340,7 +344,7 @@ class TestFileLibTiff(LibTiffTestCase):
if 700 in reloaded.tag_v2: if 700 in reloaded.tag_v2:
assert reloaded.tag_v2[700] == b"xmlpacket tag" assert reloaded.tag_v2[700] == b"xmlpacket tag"
def test_int_dpi(self, tmp_path): def test_int_dpi(self, tmp_path: Path) -> None:
# issue #1765 # issue #1765
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -350,7 +354,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.info["dpi"] == (72.0, 72.0) 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: with Image.open("Tests/images/hopper_g4_500.tif") as i:
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
i.save(out, compression="group3") i.save(out, compression="group3")
@ -359,7 +363,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert reread.info["compression"] == "group3" assert reread.info["compression"] == "group3"
assert_image_equal(reread, i) 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: with Image.open("Tests/images/16bit.deflate.tif") as im:
assert im.getpixel((0, 0)) == 480 assert im.getpixel((0, 0)) == 480
assert im.mode == "I;16" assert im.mode == "I;16"
@ -378,7 +382,7 @@ class TestFileLibTiff(LibTiffTestCase):
# UNDONE - libtiff defaults to writing in native endian, so # UNDONE - libtiff defaults to writing in native endian, so
# on big endian, we'll get back mode = 'I;16B' here. # 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: with Image.open("Tests/images/16bit.MM.deflate.tif") as im:
assert im.getpixel((0, 0)) == 480 assert im.getpixel((0, 0)) == 480
assert im.mode == "I;16B" assert im.mode == "I;16B"
@ -395,7 +399,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert reread.info["compression"] == im.info["compression"] assert reread.info["compression"] == im.info["compression"]
assert reread.getpixel((0, 0)) == 480 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""" """Tests String data in info directory"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig: with Image.open(test_file) as orig:
@ -408,7 +412,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag_v2[269]
assert "temp.tif" == reread.tag[269][0] assert "temp.tif" == reread.tag[269][0]
def test_12bit_rawmode(self): def test_12bit_rawmode(self) -> None:
"""Are we generating the same interpretation """Are we generating the same interpretation
of the image as Imagemagick is?""" of the image as Imagemagick is?"""
TiffImagePlugin.READ_LIBTIFF = True TiffImagePlugin.READ_LIBTIFF = True
@ -423,7 +427,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") 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 # test case from irc, how to do blur on b/w image
# and save to compressed tif. # and save to compressed tif.
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -435,7 +439,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, out) 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 # Test various tiff compressions and assert similar image content but reduced
# file sizes. # file sizes.
im = hopper("RGB") im = hopper("RGB")
@ -461,7 +465,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert size_compressed > size_jpeg assert size_compressed > size_jpeg
assert size_jpeg > size_jpeg_30 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") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
im.save(out, compression="tiff_jpeg") im.save(out, compression="tiff_jpeg")
@ -469,7 +473,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.info["compression"] == "jpeg" 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") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
im.save(out, compression="tiff_deflate") im.save(out, compression="tiff_deflate")
@ -477,7 +481,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.info["compression"] == "tiff_adobe_deflate" assert reloaded.info["compression"] == "tiff_adobe_deflate"
def test_quality(self, tmp_path): def test_quality(self, tmp_path: Path) -> None:
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") 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=0)
im.save(out, compression="jpeg", quality=100) 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") im = hopper("CMYK")
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -500,7 +504,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) @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") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True TiffImagePlugin.WRITE_LIBTIFF = True
@ -512,14 +516,14 @@ class TestFileLibTiff(LibTiffTestCase):
assert len(reloaded.tag_v2[320]) == 768 assert len(reloaded.tag_v2[320]) == 768
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
def test_bw_compression_w_rgb(self, compression, tmp_path): def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(out, compression=compression) 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") im = Image.open("Tests/images/hopper_g4_500.tif")
fn = im.fp.fileno() fn = im.fp.fileno()
@ -533,7 +537,7 @@ class TestFileLibTiff(LibTiffTestCase):
with pytest.raises(OSError): with pytest.raises(OSError):
os.close(fn) os.close(fn)
def test_multipage(self): def test_multipage(self) -> None:
# issue #862 # issue #862
TiffImagePlugin.READ_LIBTIFF = True TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
@ -556,7 +560,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False
def test_multipage_nframes(self): def test_multipage_nframes(self) -> None:
# issue #862 # issue #862
TiffImagePlugin.READ_LIBTIFF = True TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
@ -569,7 +573,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False
def test_multipage_seek_backwards(self): def test_multipage_seek_backwards(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
im.seek(1) im.seek(1)
@ -580,14 +584,14 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False
def test__next(self): def test__next(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
assert not im.tag.next assert not im.tag.next
im.load() im.load()
assert not im.tag.next assert not im.tag.next
def test_4bit(self): def test_4bit(self) -> None:
# Arrange # Arrange
test_file = "Tests/images/hopper_gray_4bpp.tif" test_file = "Tests/images/hopper_gray_4bpp.tif"
original = hopper("L") original = hopper("L")
@ -602,7 +606,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.mode == "L" assert im.mode == "L"
assert_image_similar(im, original, 7.3) assert_image_similar(im, original, 7.3)
def test_gray_semibyte_per_pixel(self): def test_gray_semibyte_per_pixel(self) -> None:
test_files = ( test_files = (
( (
24.8, # epsilon 24.8, # epsilon
@ -635,7 +639,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im2.mode == "L" assert im2.mode == "L"
assert_image_equal(im, im2) assert_image_equal(im, im2)
def test_save_bytesio(self): def test_save_bytesio(self) -> None:
# PR 1011 # PR 1011
# Test TIFF saving to io.BytesIO() object. # Test TIFF saving to io.BytesIO() object.
@ -645,7 +649,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Generate test image # Generate test image
pilim = hopper() pilim = hopper()
def save_bytesio(compression=None): def save_bytesio(compression: str | None = None) -> None:
buffer_io = io.BytesIO() buffer_io = io.BytesIO()
pilim.save(buffer_io, format="tiff", compression=compression) pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0) buffer_io.seek(0)
@ -660,7 +664,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.WRITE_LIBTIFF = False
TiffImagePlugin.READ_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") im = hopper("YCbCr")
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, compression="jpeg") im.save(outfile, compression="jpeg")
@ -669,7 +673,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
def test_exif_ifd(self, tmp_path): def test_exif_ifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert im.tag_v2[34665] == 125456 assert im.tag_v2[34665] == 125456
@ -679,7 +683,7 @@ class TestFileLibTiff(LibTiffTestCase):
if Image.core.libtiff_support_custom_tags: if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456 assert reloaded.tag_v2[34665] == 125456
def test_crashing_metadata(self, tmp_path): def test_crashing_metadata(self, tmp_path: Path) -> None:
# issue 1597 # issue 1597
with Image.open("Tests/images/rdf.tif") as im: with Image.open("Tests/images/rdf.tif") as im:
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -689,7 +693,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, format="TIFF") im.save(out, format="TIFF")
TiffImagePlugin.WRITE_LIBTIFF = False 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 # Issue 973
# Test TIFF with tag 297 (Page Number) having value of 0 0. # Test TIFF with tag 297 (Page Number) having value of 0 0.
# The first number is the current page number. # The first number is the current page number.
@ -703,7 +707,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not divide by zero # Should not divide by zero
im.save(outfile) 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 # https://github.com/python-pillow/Pillow/issues/1651
tmpfile = str(tmp_path / "temp.tif") tmpfile = str(tmp_path / "temp.tif")
@ -717,7 +721,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise PermissionError. # Should not raise PermissionError.
os.remove(tmpfile) os.remove(tmpfile)
def test_read_icc(self): def test_read_icc(self) -> None:
with Image.open("Tests/images/hopper.iccprofile.tif") as img: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc = img.info.get("icc_profile") icc = img.info.get("icc_profile")
assert icc is not None assert icc is not None
@ -728,8 +732,8 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False
assert icc == icc_libtiff assert icc == icc_libtiff
def test_write_icc(self, tmp_path): def test_write_icc(self, tmp_path: Path) -> None:
def check_write(libtiff): def check_write(libtiff: bool) -> None:
TiffImagePlugin.WRITE_LIBTIFF = libtiff TiffImagePlugin.WRITE_LIBTIFF = libtiff
with Image.open("Tests/images/hopper.iccprofile.tif") as img: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
@ -748,7 +752,7 @@ class TestFileLibTiff(LibTiffTestCase):
for libtiff in libtiffs: for libtiff in libtiffs:
check_write(libtiff) check_write(libtiff)
def test_multipage_compression(self): def test_multipage_compression(self) -> None:
with Image.open("Tests/images/compression.tif") as im: with Image.open("Tests/images/compression.tif") as im:
im.seek(0) im.seek(0)
assert im._compression == "tiff_ccitt" assert im._compression == "tiff_ccitt"
@ -764,7 +768,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (10, 10) assert im.size == (10, 10)
im.load() im.load()
def test_save_tiff_with_jpegtables(self, tmp_path): def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None:
# Arrange # Arrange
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
@ -776,7 +780,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise UnicodeDecodeError or anything else # Should not raise UnicodeDecodeError or anything else
im.save(outfile) 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: with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (100, 40) assert im.size == (100, 40)
@ -792,7 +796,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") 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: with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im:
assert im.mode == "RGBA" assert im.mode == "RGBA"
assert im.size == (100, 40) assert im.size == (100, 40)
@ -804,7 +808,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
@skip_unless_feature("jpg") @skip_unless_feature("jpg")
def test_gimp_tiff(self): def test_gimp_tiff(self) -> None:
# Read TIFF JPEG images from GIMP [@PIL168] # Read TIFF JPEG images from GIMP [@PIL168]
filename = "Tests/images/pil168.tif" filename = "Tests/images/pil168.tif"
with Image.open(filename) as im: with Image.open(filename) as im:
@ -817,14 +821,14 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/pil168.png") 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 # https://github.com/python-pillow/Pillow/issues/1466
with Image.open("Tests/images/copyleft.tiff") as im: with Image.open("Tests/images/copyleft.tiff") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
assert_image_equal_tofile(im, "Tests/images/copyleft.png", 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)) im = Image.new("F", (1, 1))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True TiffImagePlugin.WRITE_LIBTIFF = True
@ -835,7 +839,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.mode == "F" assert reloaded.mode == "F"
assert reloaded.getexif()[SAMPLEFORMAT] == 3 assert reloaded.getexif()[SAMPLEFORMAT] == 3
def test_lzma(self, capfd): def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None:
try: try:
with Image.open("Tests/images/hopper_lzma.tif") as im: with Image.open("Tests/images/hopper_lzma.tif") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
@ -851,7 +855,7 @@ class TestFileLibTiff(LibTiffTestCase):
sys.stderr.write(captured.err) sys.stderr.write(captured.err)
raise raise
def test_webp(self, capfd): def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None:
try: try:
with Image.open("Tests/images/hopper_webp.tif") as im: with Image.open("Tests/images/hopper_webp.tif") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
@ -873,7 +877,7 @@ class TestFileLibTiff(LibTiffTestCase):
sys.stderr.write(captured.err) sys.stderr.write(captured.err)
raise raise
def test_lzw(self): def test_lzw(self) -> None:
with Image.open("Tests/images/hopper_lzw.tif") as im: with Image.open("Tests/images/hopper_lzw.tif") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (128, 128) assert im.size == (128, 128)
@ -881,12 +885,12 @@ class TestFileLibTiff(LibTiffTestCase):
im2 = hopper() im2 = hopper()
assert_image_similar(im, im2, 5) 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" infile = "Tests/images/tiff_strip_cmyk_jpeg.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) 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" infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5)
@ -894,7 +898,7 @@ class TestFileLibTiff(LibTiffTestCase):
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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" infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2)
@ -902,12 +906,12 @@ class TestFileLibTiff(LibTiffTestCase):
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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" infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) 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" infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5)
@ -915,7 +919,7 @@ class TestFileLibTiff(LibTiffTestCase):
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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" infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01)
@ -923,45 +927,45 @@ class TestFileLibTiff(LibTiffTestCase):
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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" infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) 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 \ # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_strip_raw.tif tiff_strip_planar_lzw.tiff # tiff_strip_raw.tif tiff_strip_planar_lzw.tiff
infile = "Tests/images/tiff_strip_planar_lzw.tiff" infile = "Tests/images/tiff_strip_planar_lzw.tiff"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") 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 \ # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff # tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff
infile = "Tests/images/tiff_tiled_planar_lzw.tiff" infile = "Tests/images/tiff_tiled_planar_lzw.tiff"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") 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 \ # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff # tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff
with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im: 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") 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 \ # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
# tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff # tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff
with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im: 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") 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 \ # gdal_translate -co TILED=yes \
# -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \
# tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff # tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff
with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im: 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") 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 \ # gdal_translate -co TILED=no \
# -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \
# tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff # 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") assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
@pytest.mark.parametrize("compression", (None, "jpeg")) @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() im = hopper()
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -985,11 +989,11 @@ class TestFileLibTiff(LibTiffTestCase):
for tag in tags: for tag in tags:
assert tag not in reloaded.getexif() 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: with Image.open("Tests/images/old-style-jpeg-compression.tif") as im:
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
def test_open_missing_samplesperpixel(self): def test_open_missing_samplesperpixel(self) -> None:
with Image.open( with Image.open(
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif"
) as im: ) 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: with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode assert im.mode == mode
assert im.size == size assert im.size == size
assert im.tile == tile assert im.tile == tile
im.load() 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 # This image does not have a RowsPerStrip TIFF tag
infile = "Tests/images/no_rows_per_strip.tif" infile = "Tests/images/no_rows_per_strip.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
im.load() im.load()
assert im.size == (950, 975) 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: with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9): for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
@ -1043,7 +1049,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_similar(base_im, im, 0.7) 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: with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9): for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
@ -1052,7 +1058,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_similar(base_im, im, 0.7) assert_image_similar(base_im, im, 0.7)
@pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") @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 # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted
# when saving to a new file. # when saving to a new file.
# Pillow 6.0 fails with "OSError: cannot identify image file". # Pillow 6.0 fails with "OSError: cannot identify image file".
@ -1073,7 +1079,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as im: with Image.open(out) as im:
im.load() im.load()
def test_realloc_overflow(self): def test_realloc_overflow(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e: with pytest.raises(OSError) as e:
@ -1084,7 +1090,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) @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)) im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
im.save(out, compression=compression) im.save(out, compression=compression)
@ -1094,14 +1100,14 @@ class TestFileLibTiff(LibTiffTestCase):
assert len(im.tag_v2[STRIPOFFSETS]) > 1 assert len(im.tag_v2[STRIPOFFSETS]) > 1
@pytest.mark.parametrize("argument", (True, False)) @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)) im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
if not argument: if not argument:
TiffImagePlugin.STRIP_SIZE = 2**18 TiffImagePlugin.STRIP_SIZE = 2**18
try: try:
arguments = {"compression": "tiff_adobe_deflate"} arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument: if argument:
arguments["strip_size"] = 2**18 arguments["strip_size"] = 2**18
im.save(out, **arguments) im.save(out, **arguments)
@ -1112,13 +1118,13 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.STRIP_SIZE = 65536 TiffImagePlugin.STRIP_SIZE = 65536
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) @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)) im = Image.new("RGB", (0, 0))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
with pytest.raises(SystemError): with pytest.raises(SystemError):
im.save(out, compression=compression) im.save(out, compression=compression)
def test_save_many_compressed(self, tmp_path): def test_save_many_compressed(self, tmp_path: Path) -> None:
im = hopper() im = hopper()
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
for _ in range(10000): for _ in range(10000):
@ -1132,7 +1138,7 @@ class TestFileLibTiff(LibTiffTestCase):
("Tests/images/child_ifd_jpeg.tiff", (20,)), ("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: with Image.open(path) as im:
ims = im.get_child_images() ims = im.get_child_images()

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
from pathlib import Path
from PIL import Image from PIL import Image
@ -7,7 +9,6 @@ from .test_file_libtiff import LibTiffTestCase
class TestFileLibTiffSmall(LibTiffTestCase): class TestFileLibTiffSmall(LibTiffTestCase):
"""The small lena image was failing on open in the libtiff """The small lena image was failing on open in the libtiff
decoder because the file pointer was set to the wrong place decoder because the file pointer was set to the wrong place
by a spurious seek. It wasn't failing with the byteio method. 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 file just before reading in libtiff. These tests remain
to ensure that it stays fixed.""" 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""" """Testing the open file load path"""
test_file = "Tests/images/hopper_g4.tif" test_file = "Tests/images/hopper_g4.tif"
@ -25,7 +26,7 @@ class TestFileLibTiffSmall(LibTiffTestCase):
assert im.size == (128, 128) assert im.size == (128, 128)
self._assert_noerr(tmp_path, im) 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""" """Testing the bytesio loading code path"""
test_file = "Tests/images/hopper_g4.tif" test_file = "Tests/images/hopper_g4.tif"
s = BytesIO() s = BytesIO()
@ -36,7 +37,7 @@ class TestFileLibTiffSmall(LibTiffTestCase):
assert im.size == (128, 128) assert im.size == (128, 128)
self._assert_noerr(tmp_path, im) 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.""" """The 128x128 lena image failed for some reason."""
test_file = "Tests/images/hopper_g4.tif" test_file = "Tests/images/hopper_g4.tif"

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, McIdasImagePlugin from PIL import Image, McIdasImagePlugin
@ -6,14 +7,14 @@ from PIL import Image, McIdasImagePlugin
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
McIdasImagePlugin.McIdasImageFile(invalid_file) McIdasImagePlugin.McIdasImageFile(invalid_file)
def test_valid_file(): def test_valid_file() -> None:
# Arrange # Arrange
# https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8
# https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, ImagePalette from PIL import Image, ImagePalette
@ -12,7 +13,7 @@ pytestmark = skip_unless_feature("libtiff")
TEST_FILE = "Tests/images/hopper.mic" TEST_FILE = "Tests/images/hopper.mic"
def test_sanity(): def test_sanity() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()
assert im.mode == "RGBA" assert im.mode == "RGBA"
@ -27,22 +28,22 @@ def test_sanity():
assert_image_similar(im, im2, 10) assert_image_similar(im, im2, 10)
def test_n_frames(): def test_n_frames() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.n_frames == 1 assert im.n_frames == 1
def test_is_animated(): def test_is_animated() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert not im.is_animated assert not im.is_animated
def test_tell(): def test_tell() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.tell() == 0 assert im.tell() == 0
def test_seek(): def test_seek() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.seek(0) im.seek(0)
assert im.tell() == 0 assert im.tell() == 0
@ -52,7 +53,7 @@ def test_seek():
assert im.tell() == 0 assert im.tell() == 0
def test_close(): def test_close() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
pass pass
assert im.ole.fp.closed assert im.ole.fp.closed
@ -62,7 +63,7 @@ def test_close():
assert im.ole.fp.closed assert im.ole.fp.closed
def test_invalid_file(): def test_invalid_file() -> None:
# Test an invalid OLE file # Test an invalid OLE file
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):

View File

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO
from typing import Any
import pytest import pytest
@ -18,7 +20,7 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg") pytestmark = skip_unless_feature("jpg")
def roundtrip(im, **options): def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO() out = BytesIO()
im.save(out, "MPO", **options) im.save(out, "MPO", **options)
test_bytes = out.tell() test_bytes = out.tell()
@ -29,7 +31,7 @@ def roundtrip(im, **options):
@pytest.mark.parametrize("test_file", test_files) @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: with Image.open(test_file) as im:
im.load() im.load()
assert im.mode == "RGB" assert im.mode == "RGB"
@ -38,8 +40,8 @@ def test_sanity(test_file):
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(): def test_unclosed_file() -> None:
def open(): def open() -> None:
im = Image.open(test_files[0]) im = Image.open(test_files[0])
im.load() im.load()
@ -47,14 +49,14 @@ def test_unclosed_file():
open() open()
def test_closed_file(): def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
im = Image.open(test_files[0]) im = Image.open(test_files[0])
im.load() im.load()
im.close() im.close()
def test_seek_after_close(): def test_seek_after_close() -> None:
im = Image.open(test_files[0]) im = Image.open(test_files[0])
im.close() im.close()
@ -62,14 +64,14 @@ def test_seek_after_close():
im.seek(1) im.seek(1)
def test_context_manager(): def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(test_files[0]) as im: with Image.open(test_files[0]) as im:
im.load() im.load()
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_app(test_file): def test_app(test_file: str) -> None:
# Test APP/COM reader (@PIL135) # Test APP/COM reader (@PIL135)
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.applist[0][0] == "APP1" assert im.applist[0][0] == "APP1"
@ -81,7 +83,7 @@ def test_app(test_file):
@pytest.mark.parametrize("test_file", test_files) @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: with Image.open(test_file) as im_original:
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) 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 assert info[34665] == 188
def test_frame_size(): def test_frame_size() -> None:
# This image has been hexedited to contain a different size # This image has been hexedited to contain a different size
# in the EXIF data of the second frame # in the EXIF data of the second frame
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
@ -105,7 +107,7 @@ def test_frame_size():
assert im.size == (640, 480) assert im.size == (640, 480)
def test_ignore_frame_size(): def test_ignore_frame_size() -> None:
# Ignore the different size of the second frame # Ignore the different size of the second frame
# since this is not a "Large Thumbnail" image # since this is not a "Large Thumbnail" image
with Image.open("Tests/images/ignore_frame_size.mpo") as im: 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) assert im.size == (64, 64)
def test_parallax(): def test_parallax() -> None:
# Nintendo # Nintendo
with Image.open("Tests/images/sugarshack.mpo") as im: with Image.open("Tests/images/sugarshack.mpo") as im:
exif = im.getexif() exif = im.getexif()
@ -132,7 +134,7 @@ def test_parallax():
assert exif.get_ifd(0x927C)[0xB211] == -3.125 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: with Image.open("Tests/images/sugarshack.mpo") as im:
exif = im.getexif() exif = im.getexif()
del exif[296] del exif[296]
@ -142,14 +144,14 @@ def test_reload_exif_after_seek():
@pytest.mark.parametrize("test_file", test_files) @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: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo[45056] == b"0100" assert mpinfo[45056] == b"0100"
assert mpinfo[45057] == 2 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 # This image has been manually hexedited to have an IFD offset of 10
# in APP2 data, in contrast to normal 8 # in APP2 data, in contrast to normal 8
with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im:
@ -158,7 +160,7 @@ def test_mp_offset():
assert mpinfo[45057] == 2 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 # This image has been manually hexedited to have the second frame
# beyond the end of the file # beyond the end of the file
with Image.open("Tests/images/sugarshack_no_data.mpo") as im: 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) @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: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
for frame_number, mpentry in enumerate(mpinfo[0xB002]): 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) @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: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
# prior to first image raises an error, both blatant and borderline # prior to first image raises an error, both blatant and borderline
@ -208,13 +210,13 @@ def test_seek(test_file):
assert im.tell() == 0 assert im.tell() == 0
def test_n_frames(): def test_n_frames() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im: with Image.open("Tests/images/sugarshack.mpo") as im:
assert im.n_frames == 2 assert im.n_frames == 2
assert im.is_animated assert im.is_animated
def test_eoferror(): def test_eoferror() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im: with Image.open("Tests/images/sugarshack.mpo") as im:
n_frames = im.n_frames n_frames = im.n_frames
@ -228,7 +230,7 @@ def test_eoferror():
@pytest.mark.parametrize("test_file", test_files) @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: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
im0 = im.tobytes() im0 = im.tobytes()
@ -243,7 +245,7 @@ def test_image_grab(test_file):
@pytest.mark.parametrize("test_file", test_files) @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: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
jpg0 = roundtrip(im) jpg0 = roundtrip(im)
@ -254,7 +256,7 @@ def test_save(test_file):
assert_image_similar(im, jpg1, 30) assert_image_similar(im, jpg1, 30)
def test_save_all(): def test_save_all() -> None:
for test_file in test_files: for test_file in test_files:
with Image.open(test_file) as im: with Image.open(test_file) as im:
im_reloaded = roundtrip(im, save_all=True) im_reloaded = roundtrip(im, save_all=True)

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
from pathlib import Path
import pytest import pytest
@ -12,7 +14,7 @@ EXTRA_DIR = "Tests/images/picins"
YA_EXTRA_DIR = "Tests/images/msp" 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") test_file = str(tmp_path / "temp.msp")
hopper("1").save(test_file) hopper("1").save(test_file)
@ -24,14 +26,14 @@ def test_sanity(tmp_path):
assert im.format == "MSP" assert im.format == "MSP"
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
MspImagePlugin.MspImageFile(invalid_file) MspImagePlugin.MspImageFile(invalid_file)
def test_bad_checksum(): def test_bad_checksum() -> None:
# Arrange # Arrange
# This was created by forcing Pillow to save with checksum=0 # This was created by forcing Pillow to save with checksum=0
bad_checksum = "Tests/images/hopper_bad_checksum.msp" bad_checksum = "Tests/images/hopper_bad_checksum.msp"
@ -41,7 +43,7 @@ def test_bad_checksum():
MspImagePlugin.MspImageFile(bad_checksum) MspImagePlugin.MspImageFile(bad_checksum)
def test_open_windows_v1(): def test_open_windows_v1() -> None:
# Arrange # Arrange
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
@ -50,7 +52,7 @@ def test_open_windows_v1():
assert isinstance(im, MspImagePlugin.MspImageFile) assert isinstance(im, MspImagePlugin.MspImageFile)
def _assert_file_image_equal(source_path, target_path): def _assert_file_image_equal(source_path, target_path) -> None:
with Image.open(source_path) as im: with Image.open(source_path) as im:
assert_image_equal_tofile(im, target_path) assert_image_equal_tofile(im, target_path)
@ -58,7 +60,7 @@ def _assert_file_image_equal(source_path, target_path):
@pytest.mark.skipif( @pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
) )
def test_open_windows_v2(): def test_open_windows_v2() -> None:
files = ( files = (
os.path.join(EXTRA_DIR, f) os.path.join(EXTRA_DIR, f)
for f in os.listdir(EXTRA_DIR) for f in os.listdir(EXTRA_DIR)
@ -71,7 +73,7 @@ def test_open_windows_v2():
@pytest.mark.skipif( @pytest.mark.skipif(
not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" 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): for f in os.listdir(YA_EXTRA_DIR):
if ".MSP" not in f: if ".MSP" not in f:
continue continue
@ -79,7 +81,7 @@ def test_msp_v2():
_assert_file_image_equal(path, path.replace(".MSP", ".png")) _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 # Arrange
im = hopper() im = hopper()
filename = str(tmp_path / "temp.msp") filename = str(tmp_path / "temp.msp")

View File

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import os.path import os.path
import subprocess import subprocess
from pathlib import Path
import pytest import pytest
@ -9,7 +11,7 @@ from PIL import Image
from .helper import assert_image_equal, hopper, magick_command 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 # Arrange
im = hopper(mode) im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".palm")) 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 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") outfile = str(tmp_path / "temp.png")
rc = subprocess.call( rc = subprocess.call(
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
@ -31,7 +33,7 @@ def open_with_magick(magick, tmp_path, f):
return Image.open(outfile) return Image.open(outfile)
def roundtrip(tmp_path, mode): def roundtrip(tmp_path: Path, mode: str) -> None:
magick = magick_command() magick = magick_command()
if not magick: if not magick:
return return
@ -44,7 +46,7 @@ def roundtrip(tmp_path, mode):
assert_image_equal(converted, im) assert_image_equal(converted, im)
def test_monochrome(tmp_path): def test_monochrome(tmp_path: Path) -> None:
# Arrange # Arrange
mode = "1" mode = "1"
@ -54,7 +56,7 @@ def test_monochrome(tmp_path):
@pytest.mark.xfail(reason="Palm P image is wrong") @pytest.mark.xfail(reason="Palm P image is wrong")
def test_p_mode(tmp_path): def test_p_mode(tmp_path: Path) -> None:
# Arrange # Arrange
mode = "P" mode = "P"
@ -64,6 +66,6 @@ def test_p_mode(tmp_path):
@pytest.mark.parametrize("mode", ("L", "RGB")) @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): with pytest.raises(OSError):
helper_save_as_palm(tmp_path, mode) helper_save_as_palm(tmp_path, mode)

View File

@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image
def test_load_raw(): def test_load_raw() -> None:
with Image.open("Tests/images/hopper.pcd") as im: with Image.open("Tests/images/hopper.pcd") as im:
im.load() # should not segfault. im.load() # should not segfault.

View File

@ -1,4 +1,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import Image, ImageFile, PcxImagePlugin from PIL import Image, ImageFile, PcxImagePlugin
@ -6,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
def _roundtrip(tmp_path, im): def _roundtrip(tmp_path: Path, im) -> None:
f = str(tmp_path / "temp.pcx") f = str(tmp_path / "temp.pcx")
im.save(f) im.save(f)
with Image.open(f) as im2: with Image.open(f) as im2:
@ -17,7 +20,7 @@ def _roundtrip(tmp_path, im):
assert_image_equal(im2, 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"): for mode in ("1", "L", "P", "RGB"):
_roundtrip(tmp_path, hopper(mode)) _roundtrip(tmp_path, hopper(mode))
@ -33,7 +36,7 @@ def test_sanity(tmp_path):
im.save(f) im.save(f)
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
@ -41,7 +44,7 @@ def test_invalid_file():
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
def test_odd(tmp_path, mode): def test_odd(tmp_path: Path, mode) -> None:
# See issue #523, odd sized images should have a stride that's even. # See issue #523, odd sized images should have a stride that's even.
# Not that ImageMagick or GIMP write PCX that way. # Not that ImageMagick or GIMP write PCX that way.
# We were not handling properly. # We were not handling properly.
@ -50,7 +53,7 @@ def test_odd(tmp_path, mode):
_roundtrip(tmp_path, hopper(mode).resize((511, 511))) _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 # Reading an image with an odd stride, making it malformed
with Image.open("Tests/images/odd_stride.pcx") as im: with Image.open("Tests/images/odd_stride.pcx") as im:
im.load() im.load()
@ -58,7 +61,7 @@ def test_odd_read():
assert im.size == (371, 150) assert im.size == (371, 150)
def test_pil184(): def test_pil184() -> None:
# Check reading of files where xmin/xmax is not zero. # Check reading of files where xmin/xmax is not zero.
test_file = "Tests/images/pil184.pcx" test_file = "Tests/images/pil184.pcx"
@ -70,7 +73,7 @@ def test_pil184():
assert im.histogram()[0] + im.histogram()[255] == 447 * 144 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)) im = Image.new("L", (1, 256))
px = im.load() px = im.load()
for y in range(256): for y in range(256):
@ -78,7 +81,7 @@ def test_1px_width(tmp_path):
_roundtrip(tmp_path, im) _roundtrip(tmp_path, im)
def test_large_count(tmp_path): def test_large_count(tmp_path: Path) -> None:
im = Image.new("L", (256, 1)) im = Image.new("L", (256, 1))
px = im.load() px = im.load()
for x in range(256): for x in range(256):
@ -86,7 +89,7 @@ def test_large_count(tmp_path):
_roundtrip(tmp_path, im) _roundtrip(tmp_path, im)
def _test_buffer_overflow(tmp_path, im, size=1024): def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None:
_last = ImageFile.MAXBLOCK _last = ImageFile.MAXBLOCK
ImageFile.MAXBLOCK = size ImageFile.MAXBLOCK = size
try: try:
@ -95,7 +98,7 @@ def _test_buffer_overflow(tmp_path, im, size=1024):
ImageFile.MAXBLOCK = _last 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)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
for y in range(4): for y in range(4):
@ -104,7 +107,7 @@ def test_break_in_count_overflow(tmp_path):
_test_buffer_overflow(tmp_path, im) _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)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
for y in range(5): for y in range(5):
@ -113,7 +116,7 @@ def test_break_one_in_loop(tmp_path):
_test_buffer_overflow(tmp_path, im) _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)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
for y in range(4): for y in range(4):
@ -124,7 +127,7 @@ def test_break_many_in_loop(tmp_path):
_test_buffer_overflow(tmp_path, im) _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)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
for y in range(5): for y in range(5):
@ -134,7 +137,7 @@ def test_break_one_at_end(tmp_path):
_test_buffer_overflow(tmp_path, im) _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)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
for y in range(5): for y in range(5):
@ -146,7 +149,7 @@ def test_break_many_at_end(tmp_path):
_test_buffer_overflow(tmp_path, im) _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)) im = Image.new("L", (257, 5))
px = im.load() px = im.load()
for y in range(5): for y in range(5):

View File

@ -1,9 +1,11 @@
from __future__ import annotations from __future__ import annotations
import io import io
import os import os
import os.path import os.path
import tempfile import tempfile
import time import time
from pathlib import Path
import pytest import pytest
@ -12,7 +14,7 @@ from PIL import Image, PdfParser, features
from .helper import hopper, mark_if_feature_version, skip_unless_feature from .helper import hopper, mark_if_feature_version, skip_unless_feature
def helper_save_as_pdf(tmp_path, mode, **kwargs): def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
# Arrange # Arrange
im = hopper(mode) im = hopper(mode)
outfile = str(tmp_path / ("temp_" + mode + ".pdf")) outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
@ -39,17 +41,17 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs):
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
def test_save(tmp_path, mode): def test_save(tmp_path: Path, mode) -> None:
helper_save_as_pdf(tmp_path, mode) helper_save_as_pdf(tmp_path, mode)
@skip_unless_feature("jpg_2000") @skip_unless_feature("jpg_2000")
@pytest.mark.parametrize("mode", ("LA", "RGBA")) @pytest.mark.parametrize("mode", ("LA", "RGBA"))
def test_save_alpha(tmp_path, mode): def test_save_alpha(tmp_path: Path, mode) -> None:
helper_save_as_pdf(tmp_path, mode) helper_save_as_pdf(tmp_path, mode)
def test_p_alpha(tmp_path): def test_p_alpha(tmp_path: Path) -> None:
# Arrange # Arrange
outfile = str(tmp_path / "temp.pdf") outfile = str(tmp_path / "temp.pdf")
with Image.open("Tests/images/pil123p.png") as im: with Image.open("Tests/images/pil123p.png") as im:
@ -65,7 +67,7 @@ def test_p_alpha(tmp_path):
assert b"\n/SMask " in contents assert b"\n/SMask " in contents
def test_monochrome(tmp_path): def test_monochrome(tmp_path: Path) -> None:
# Arrange # Arrange
mode = "1" mode = "1"
@ -74,7 +76,7 @@ def test_monochrome(tmp_path):
assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) 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") im = hopper("PA")
outfile = str(tmp_path / "temp_PA.pdf") outfile = str(tmp_path / "temp_PA.pdf")
@ -82,7 +84,7 @@ def test_unsupported_mode(tmp_path):
im.save(outfile) im.save(outfile)
def test_resolution(tmp_path): def test_resolution(tmp_path: Path) -> None:
im = hopper() im = hopper()
outfile = str(tmp_path / "temp.pdf") outfile = str(tmp_path / "temp.pdf")
@ -110,7 +112,7 @@ def test_resolution(tmp_path):
{"dpi": (75, 150), "resolution": 200}, {"dpi": (75, 150), "resolution": 200},
), ),
) )
def test_dpi(params, tmp_path): def test_dpi(params, tmp_path: Path) -> None:
im = hopper() im = hopper()
outfile = str(tmp_path / "temp.pdf") outfile = str(tmp_path / "temp.pdf")
@ -134,7 +136,7 @@ def test_dpi(params, tmp_path):
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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 # Single frame image
helper_save_as_pdf(tmp_path, "RGB", save_all=True) helper_save_as_pdf(tmp_path, "RGB", save_all=True)
@ -170,7 +172,7 @@ def test_save_all(tmp_path):
assert os.path.getsize(outfile) > 0 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 # Test saving a multiframe image without save_all
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
outfile = str(tmp_path / "temp.pdf") outfile = str(tmp_path / "temp.pdf")
@ -180,7 +182,7 @@ def test_multiframe_normal_save(tmp_path):
assert os.path.getsize(outfile) > 0 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 # fail on a buffer full of null bytes
with pytest.raises(PdfParser.PdfFormatError): with pytest.raises(PdfParser.PdfFormatError):
PdfParser.PdfParser(buf=bytearray(65536)) PdfParser.PdfParser(buf=bytearray(65536))
@ -217,14 +219,14 @@ def test_pdf_open(tmp_path):
assert not hopper_pdf.should_close_file 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") im = hopper("RGB")
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
def check_pdf_pages_consistency(pdf): def check_pdf_pages_consistency(pdf) -> None:
pages_info = pdf.read_indirect(pdf.pages_ref) pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info assert b"Parent" not in pages_info
assert b"Kids" in pages_info assert b"Kids" in pages_info
@ -242,7 +244,7 @@ def check_pdf_pages_consistency(pdf):
assert kids_not_used == [] assert kids_not_used == []
def test_pdf_append(tmp_path): def test_pdf_append(tmp_path: Path) -> None:
# make a PDF file # make a PDF file
pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser")
@ -293,7 +295,7 @@ def test_pdf_append(tmp_path):
check_pdf_pages_consistency(pdf) check_pdf_pages_consistency(pdf)
def test_pdf_info(tmp_path): def test_pdf_info(tmp_path: Path) -> None:
# make a PDF file # make a PDF file
pdf_filename = helper_save_as_pdf( pdf_filename = helper_save_as_pdf(
tmp_path, tmp_path,
@ -322,7 +324,7 @@ def test_pdf_info(tmp_path):
check_pdf_pages_consistency(pdf) check_pdf_pages_consistency(pdf)
def test_pdf_append_to_bytesio(): def test_pdf_append_to_bytesio() -> None:
im = hopper("RGB") im = hopper("RGB")
f = io.BytesIO() f = io.BytesIO()
im.save(f, format="PDF") im.save(f, format="PDF")
@ -337,7 +339,7 @@ def test_pdf_append_to_bytesio():
@pytest.mark.timeout(1) @pytest.mark.timeout(1)
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
@pytest.mark.parametrize("newline", (b"\r", b"\n")) @pytest.mark.parametrize("newline", (b"\r", b"\n"))
def test_redos(newline): def test_redos(newline) -> None:
malicious = b" trailer<<>>" + newline * 3456 malicious = b" trailer<<>>" + newline * 3456
# This particular exception isn't relevant here. # This particular exception isn't relevant here.

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, PixarImagePlugin from PIL import Image, PixarImagePlugin
@ -8,7 +9,7 @@ from .helper import assert_image_similar, hopper
TEST_FILE = "Tests/images/hopper.pxr" TEST_FILE = "Tests/images/hopper.pxr"
def test_sanity(): def test_sanity() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()
assert im.mode == "RGB" assert im.mode == "RGB"
@ -20,7 +21,7 @@ def test_sanity():
assert_image_similar(im, im2, 4.8) assert_image_similar(im, im2, 4.8)
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):

View File

@ -1,9 +1,12 @@
from __future__ import annotations from __future__ import annotations
import re import re
import sys import sys
import warnings import warnings
import zlib import zlib
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import Any
import pytest import pytest
@ -34,7 +37,7 @@ TEST_PNG_FILE = "Tests/images/hopper.png"
MAGIC = PngImagePlugin._MAGIC MAGIC = PngImagePlugin._MAGIC
def chunk(cid, *data): def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO() test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data) PngImagePlugin.putchunk(*(test_file, cid) + data)
return test_file.getvalue() return test_file.getvalue()
@ -50,11 +53,11 @@ HEAD = MAGIC + IHDR
TAIL = IDAT + IEND TAIL = IDAT + IEND
def load(data): def load(data: bytes) -> Image.Image:
return Image.open(BytesIO(data)) return Image.open(BytesIO(data))
def roundtrip(im, **options): def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO() out = BytesIO()
im.save(out, "PNG", **options) im.save(out, "PNG", **options)
out.seek(0) out.seek(0)
@ -63,7 +66,7 @@ def roundtrip(im, **options):
@skip_unless_feature("zlib") @skip_unless_feature("zlib")
class TestFilePng: class TestFilePng:
def get_chunks(self, filename): def get_chunks(self, filename: str) -> list[bytes]:
chunks = [] chunks = []
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
fp.read(8) fp.read(8)
@ -78,7 +81,7 @@ class TestFilePng:
png.crc(cid, s) png.crc(cid, s)
return chunks return chunks
def test_sanity(self, tmp_path): def test_sanity(self, tmp_path: Path) -> None:
# internal version number # internal version number
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
@ -101,13 +104,13 @@ class TestFilePng:
reloaded = reloaded.convert(mode) reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im) assert_image_equal(reloaded, im)
def test_invalid_file(self): def test_invalid_file(self) -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
PngImagePlugin.PngImageFile(invalid_file) PngImagePlugin.PngImageFile(invalid_file)
def test_broken(self): def test_broken(self) -> None:
# Check reading of totally broken files. In this case, the test # Check reading of totally broken files. In this case, the test
# file was checked into Subversion as a text file. # file was checked into Subversion as a text file.
@ -116,7 +119,7 @@ class TestFilePng:
with Image.open(test_file): with Image.open(test_file):
pass pass
def test_bad_text(self): def test_bad_text(self) -> None:
# Make sure PIL can read malformed tEXt chunks (@PIL152) # Make sure PIL can read malformed tEXt chunks (@PIL152)
im = load(HEAD + chunk(b"tEXt") + TAIL) im = load(HEAD + chunk(b"tEXt") + TAIL)
@ -134,7 +137,7 @@ class TestFilePng:
im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL)
assert im.info == {"spam": "egg\x00"} 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) # Test reading malformed zTXt chunks (python-pillow/Pillow#318)
im = load(HEAD + chunk(b"zTXt") + TAIL) im = load(HEAD + chunk(b"zTXt") + TAIL)
@ -155,7 +158,7 @@ class TestFilePng:
im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL)
assert im.info == {"spam": "egg"} assert im.info == {"spam": "egg"}
def test_bad_itxt(self): def test_bad_itxt(self) -> None:
im = load(HEAD + chunk(b"iTXt") + TAIL) im = load(HEAD + chunk(b"iTXt") + TAIL)
assert im.info == {} assert im.info == {}
@ -199,7 +202,7 @@ class TestFilePng:
assert im.info["spam"].lang == "en" assert im.info["spam"].lang == "en"
assert im.info["spam"].tkey == "Spam" assert im.info["spam"].tkey == "Spam"
def test_interlace(self): def test_interlace(self) -> None:
test_file = "Tests/images/pil123p.png" test_file = "Tests/images/pil123p.png"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert_image(im, "P", (162, 150)) assert_image(im, "P", (162, 150))
@ -214,7 +217,7 @@ class TestFilePng:
im.load() im.load()
def test_load_transparent_p(self): def test_load_transparent_p(self) -> None:
test_file = "Tests/images/pil123p.png" test_file = "Tests/images/pil123p.png"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert_image(im, "P", (162, 150)) assert_image(im, "P", (162, 150))
@ -224,7 +227,7 @@ class TestFilePng:
# image has 124 unique alpha values # image has 124 unique alpha values
assert len(im.getchannel("A").getcolors()) == 124 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" test_file = "Tests/images/rgb_trns.png"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.info["transparency"] == (0, 255, 52) assert im.info["transparency"] == (0, 255, 52)
@ -236,7 +239,7 @@ class TestFilePng:
# image has 876 transparent pixels # image has 876 transparent pixels
assert im.getchannel("A").getcolors()[0][0] == 876 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" in_file = "Tests/images/pil123p.png"
with Image.open(in_file) as im: with Image.open(in_file) as im:
# 'transparency' contains a byte string with the opacity for # 'transparency' contains a byte string with the opacity for
@ -257,7 +260,7 @@ class TestFilePng:
# image has 124 unique alpha values # image has 124 unique alpha values
assert len(im.getchannel("A").getcolors()) == 124 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" in_file = "Tests/images/p_trns_single.png"
with Image.open(in_file) as im: with Image.open(in_file) as im:
# pixel value 164 is full transparent # pixel value 164 is full transparent
@ -280,7 +283,7 @@ class TestFilePng:
# image has 876 transparent pixels # image has 876 transparent pixels
assert im.getchannel("A").getcolors()[0][0] == 876 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 # check if solid black image with full transparency
# is supported (check for #1838) # is supported (check for #1838)
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
@ -298,7 +301,7 @@ class TestFilePng:
assert_image(im, "RGBA", (10, 10)) assert_image(im, "RGBA", (10, 10))
assert im.getcolors() == [(100, (0, 0, 0, 0))] 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(): for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
in_file = "Tests/images/" + mode.lower() + "_trns.png" in_file = "Tests/images/" + mode.lower() + "_trns.png"
with Image.open(in_file) as im: with Image.open(in_file) as im:
@ -319,13 +322,13 @@ class TestFilePng:
test_im_rgba = test_im.convert("RGBA") test_im_rgba = test_im.convert("RGBA")
assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent 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" in_file = "Tests/images/caption_6_33_22.png"
with Image.open(in_file) as im: with Image.open(in_file) as im:
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
im.save(test_file) im.save(test_file)
def test_load_verify(self): def test_load_verify(self) -> None:
# Check open/load/verify exception (@PIL150) # Check open/load/verify exception (@PIL150)
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
@ -338,7 +341,7 @@ class TestFilePng:
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
im.verify() im.verify()
def test_verify_struct_error(self): def test_verify_struct_error(self) -> None:
# Check open/load/verify exception (#1755) # Check open/load/verify exception (#1755)
# offsets to test, -10: breaks in i32() in read. (OSError) # offsets to test, -10: breaks in i32() in read. (OSError)
@ -354,7 +357,7 @@ class TestFilePng:
with pytest.raises((OSError, SyntaxError)): with pytest.raises((OSError, SyntaxError)):
im.verify() im.verify()
def test_verify_ignores_crc_error(self): def test_verify_ignores_crc_error(self) -> None:
# check ignores crc errors in ancillary chunks # check ignores crc errors in ancillary chunks
chunk_data = chunk(b"tEXt", b"spam") chunk_data = chunk(b"tEXt", b"spam")
@ -371,7 +374,7 @@ class TestFilePng:
finally: finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False 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 # check does not ignore crc errors in required chunks
image_data = MAGIC + IHDR[:-1] + b"q" + TAIL image_data = MAGIC + IHDR[:-1] + b"q" + TAIL
@ -383,18 +386,18 @@ class TestFilePng:
finally: finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_roundtrip_dpi(self): def test_roundtrip_dpi(self) -> None:
# Check dpi roundtripping # Check dpi roundtripping
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
im = roundtrip(im, dpi=(100.33, 100.33)) im = roundtrip(im, dpi=(100.33, 100.33))
assert im.info["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: with Image.open(TEST_PNG_FILE) as im:
assert im.info["dpi"] == (95.9866, 95.9866) assert im.info["dpi"] == (95.9866, 95.9866)
def test_roundtrip_text(self): def test_roundtrip_text(self) -> None:
# Check text roundtripping # Check text roundtripping
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
@ -406,7 +409,7 @@ class TestFilePng:
assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"} assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"}
assert im.text == {"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 # Check iTXt roundtripping
im = Image.new("RGB", (32, 32)) im = Image.new("RGB", (32, 32))
@ -422,7 +425,7 @@ class TestFilePng:
assert im.text["eggs"].lang == "en" assert im.text["eggs"].lang == "en"
assert im.text["eggs"].tkey == "Eggs" 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 # Check so that non-Unicode text is saved as a tEXt rather than iTXt
im = Image.new("RGB", (32, 32)) im = Image.new("RGB", (32, 32))
@ -431,10 +434,10 @@ class TestFilePng:
im = roundtrip(im, pnginfo=info) im = roundtrip(im, pnginfo=info)
assert isinstance(im.info["Text"], str) assert isinstance(im.info["Text"], str)
def test_unicode_text(self): def test_unicode_text(self) -> None:
# Check preservation of non-ASCII characters # Check preservation of non-ASCII characters
def rt_text(value): def rt_text(value: str) -> None:
im = Image.new("RGB", (32, 32)) im = Image.new("RGB", (32, 32))
info = PngImagePlugin.PngInfo() info = PngImagePlugin.PngInfo()
info.add_text("Text", value) info.add_text("Text", value)
@ -447,7 +450,7 @@ class TestFilePng:
rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00))
rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined 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: # Check reading of evil PNG file. For information, see:
# http://scary.beasts.org/security/CESA-2004-001.txt # http://scary.beasts.org/security/CESA-2004-001.txt
# The first byte is removed from pngtest_bad.png # The first byte is removed from pngtest_bad.png
@ -461,7 +464,7 @@ class TestFilePng:
with Image.open(pngfile): with Image.open(pngfile):
pass pass
def test_trns_rgb(self): def test_trns_rgb(self) -> None:
# Check writing and reading of tRNS chunks for RGB images. # Check writing and reading of tRNS chunks for RGB images.
# Independent file sample provided by Sebastian Spaeth. # Independent file sample provided by Sebastian Spaeth.
@ -476,7 +479,7 @@ class TestFilePng:
im = roundtrip(im, transparency=(0, 1, 2)) im = roundtrip(im, transparency=(0, 1, 2))
assert im.info["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 # Check writing a transparency of 0, issue #528
im = hopper("P") im = hopper("P")
im.info["transparency"] = 0 im.info["transparency"] = 0
@ -489,13 +492,13 @@ class TestFilePng:
assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) 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 # Check reading images with null tRNS value, issue #1239
test_file = "Tests/images/tRNS_null_1x1.png" test_file = "Tests/images/tRNS_null_1x1.png"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.info["transparency"] == 0 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: with Image.open("Tests/images/icc_profile_none.png") as im:
assert im.info["icc_profile"] is None assert im.info["icc_profile"] is None
@ -505,40 +508,40 @@ class TestFilePng:
im = roundtrip(im, icc_profile=expected_icc) im = roundtrip(im, icc_profile=expected_icc)
assert im.info["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: with Image.open("Tests/images/icc_profile.png") as im:
assert "icc_profile" in im.info assert "icc_profile" in im.info
im = roundtrip(im, icc_profile=None) im = roundtrip(im, icc_profile=None)
assert "icc_profile" not in im.info 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: with Image.open("Tests/images/icc_profile.png") as im:
expected_icc = im.info["icc_profile"] expected_icc = im.info["icc_profile"]
im = roundtrip(im) im = roundtrip(im)
assert im.info["icc_profile"] == expected_icc 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: with Image.open("Tests/images/icc_profile_none.png") as im:
assert im.info["icc_profile"] is None assert im.info["icc_profile"] is None
im = roundtrip(im) im = roundtrip(im)
assert "icc_profile" not in im.info assert "icc_profile" not in im.info
def test_repr_png(self): def test_repr_png(self) -> None:
im = hopper() im = hopper()
with Image.open(BytesIO(im._repr_png_())) as repr_png: with Image.open(BytesIO(im._repr_png_())) as repr_png:
assert repr_png.format == "PNG" assert repr_png.format == "PNG"
assert_image_equal(im, repr_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") im = hopper("F")
assert im._repr_png_() is None 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: with Image.open("Tests/images/icc_profile.png") as im:
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
im.convert("P").save(test_file, dpi=(100, 100)) im.convert("P").save(test_file, dpi=(100, 100))
@ -559,17 +562,17 @@ class TestFilePng:
# pHYs - before IDAT # pHYs - before IDAT
assert chunks.index(b"pHYs") < chunks.index(b"IDAT") assert chunks.index(b"pHYs") < chunks.index(b"IDAT")
def test_getchunks(self): def test_getchunks(self) -> None:
im = hopper() im = hopper()
chunks = PngImagePlugin.getchunks(im) chunks = PngImagePlugin.getchunks(im)
assert len(chunks) == 3 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: with Image.open("Tests/images/exif.png") as im:
assert im.private_chunks == [(b"orNT", b"\x01")] 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 # Check private chunk roundtripping
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
@ -587,7 +590,7 @@ class TestFilePng:
(b"prIV", b"VALUE3", True), (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: with Image.open("Tests/images/hopper.png") as im:
assert "comment" in im.text assert "comment" in im.text
for k, v in { for k, v in {
@ -614,7 +617,7 @@ class TestFilePng:
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
def test_padded_idat(self): def test_padded_idat(self) -> None:
# This image has been manually hexedited # This image has been manually hexedited
# so that the IDAT chunk has padding at the end # so that the IDAT chunk has padding at the end
# Set MAXBLOCK to the length of the actual data # Set MAXBLOCK to the length of the actual data
@ -634,7 +637,7 @@ class TestFilePng:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") "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() fp = BytesIO()
with PngImagePlugin.PngStream(fp) as png: with PngImagePlugin.PngStream(fp) as png:
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -644,7 +647,7 @@ class TestFilePng:
png.call(cid, 0, 0) png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_specify_bits(self, tmp_path): def test_specify_bits(self, tmp_path: Path) -> None:
im = hopper("P") im = hopper("P")
out = str(tmp_path / "temp.png") out = str(tmp_path / "temp.png")
@ -653,7 +656,7 @@ class TestFilePng:
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 48 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 = Image.new("P", (1, 1))
im.putpalette((1, 1, 1)) im.putpalette((1, 1, 1))
@ -663,7 +666,7 @@ class TestFilePng:
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 3 assert len(reloaded.png.im_palette[1]) == 3
def test_getxmp(self): def test_getxmp(self) -> None:
with Image.open("Tests/images/color_snakes.png") as im: with Image.open("Tests/images/color_snakes.png") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns( with pytest.warns(
@ -678,7 +681,7 @@ class TestFilePng:
assert description["PixelXDimension"] == "10" assert description["PixelXDimension"] == "10"
assert description["subject"]["Seq"] is None assert description["subject"]["Seq"] is None
def test_exif(self): def test_exif(self) -> None:
# With an EXIF chunk # With an EXIF chunk
with Image.open("Tests/images/exif.png") as im: with Image.open("Tests/images/exif.png") as im:
exif = im._getexif() exif = im._getexif()
@ -704,7 +707,7 @@ class TestFilePng:
exif = im.getexif() exif = im.getexif()
assert exif[274] == 3 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 exif is not saved from info
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/exif.png") as im: with Image.open("Tests/images/exif.png") as im:
@ -724,7 +727,7 @@ class TestFilePng:
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" 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: with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
im.save(test_file, exif=im.getexif()) im.save(test_file, exif=im.getexif())
@ -733,7 +736,7 @@ class TestFilePng:
exif = reloaded._getexif() exif = reloaded._getexif()
assert exif[305] == "Adobe Photoshop CS Macintosh" 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: with Image.open(TEST_PNG_FILE) as im:
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
im.save(test_file, exif=b"exifstring") im.save(test_file, exif=b"exifstring")
@ -741,11 +744,11 @@ class TestFilePng:
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" 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: with Image.open(TEST_PNG_FILE) as im:
assert im.tell() == 0 assert im.tell() == 0
def test_seek(self): def test_seek(self) -> None:
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
im.seek(0) im.seek(0)
@ -753,7 +756,7 @@ class TestFilePng:
im.seek(1) im.seek(1)
@pytest.mark.parametrize("buffer", (True, False)) @pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(self, buffer): def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer: if buffer:
@ -785,7 +788,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
mem_limit = 2 * 1024 # max increase in K mem_limit = 2 * 1024 # max increase in K
iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs 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: with open("Tests/images/hopper.png", "rb") as f:
DATA = BytesIO(f.read(16 * 1024)) DATA = BytesIO(f.read(16 * 1024))
@ -793,7 +796,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
with Image.open(DATA) as im: with Image.open(DATA) as im:
im.load() im.load()
def core(): def core() -> None:
with Image.open(DATA) as im: with Image.open(DATA) as im:
im.load() im.load()

View File

@ -1,18 +1,25 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from io import BytesIO from io import BytesIO
from pathlib import Path
import pytest import pytest
from PIL import Image, PpmImagePlugin 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 # sample ppm stream
TEST_FILE = "Tests/images/hopper.ppm" TEST_FILE = "Tests/images/hopper.ppm"
def test_sanity(): def test_sanity() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (128, 128) 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) fp = BytesIO(data)
with Image.open(fp) as im: with Image.open(fp) as im:
assert im.size == (3, 1) 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 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: with Image.open("Tests/images/16_bit_binary.pgm") as im:
assert im.mode == "I" assert im.mode == "I"
assert im.size == (20, 100) 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") 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: with Image.open("Tests/images/16_bit_binary.pgm") as im:
f = str(tmp_path / "temp.pgm") filename = str(tmp_path / "temp.pgm")
im.save(f, "PPM") 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: with Image.open("Tests/images/hopper.pnm") as im:
assert_image_similar(im, hopper(), 0.0001) assert_image_similar(im, hopper(), 0.0001)
f = str(tmp_path / "temp.pnm") filename = str(tmp_path / "temp.pnm")
im.save(f) 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( @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: with Image.open(plain_path) as im:
assert_image_equal_tofile(im, raw_path) assert_image_equal_tofile(im, raw_path)
def test_16bit_plain_pgm(): def test_16bit_plain_pgm() -> None:
# P2 with maxval 2 ** 16 - 1 # P2 with maxval 2 ** 16 - 1
with Image.open("Tests/images/hopper_16bit_plain.pgm") as im: with Image.open("Tests/images/hopper_16bit_plain.pgm") as im:
assert im.mode == "I" 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), (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") path1 = str(tmp_path / "temp1.ppm")
path2 = str(tmp_path / "temp2.ppm") path2 = str(tmp_path / "temp2.ppm")
comment = b"# comment" * comment_count 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")) @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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(data) 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")) @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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(data) 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 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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(data) f.write(data)
@ -192,7 +241,7 @@ def test_plain_ppm_token_too_long(tmp_path, data):
im.load() 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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n256") f.write(b"P3\n128 128\n255\n256")
@ -202,12 +251,12 @@ def test_plain_ppm_value_too_large(tmp_path):
im.load() im.load()
def test_magic(): def test_magic() -> None:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid")) 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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n")
@ -216,7 +265,7 @@ def test_header_with_comments(tmp_path):
assert im.size == (128, 128) 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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P6\nTEST") f.write(b"P6\nTEST")
@ -226,7 +275,7 @@ def test_non_integer_token(tmp_path):
pass 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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P6\n 01234567890") f.write(b"P6\n 01234567890")
@ -238,7 +287,7 @@ def test_header_token_too_long(tmp_path):
assert str(e.value) == "Token too long in file header: 01234567890" 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 # Test EOF in header
path = str(tmp_path / "temp.pgm") path = str(tmp_path / "temp.pgm")
with open(path, "wb") as f: with open(path, "wb") as f:
@ -257,7 +306,7 @@ def test_truncated_file(tmp_path):
im.load() 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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P2 1 2 255 255") f.write(b"P2 1 2 255 255")
@ -268,7 +317,7 @@ def test_not_enough_image_data(tmp_path):
@pytest.mark.parametrize("maxval", (b"0", b"65536")) @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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P6\n3 1 " + maxval) f.write(b"P6\n3 1 " + maxval)
@ -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" 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 # Storage.c accepted negative values for xsize, ysize. the
# internal open_ppm function didn't check for sanity but it # internal open_ppm function didn't check for sanity but it
# has been removed. The default opener doesn't accept negative # has been removed. The default opener doesn't accept negative
@ -291,7 +340,7 @@ def test_neg_ppm():
pass pass
def test_mimetypes(tmp_path): def test_mimetypes(tmp_path: Path) -> None:
path = str(tmp_path / "temp.pgm") path = str(tmp_path / "temp.pgm")
with open(path, "wb") as f: with open(path, "wb") as f:
@ -306,7 +355,7 @@ def test_mimetypes(tmp_path):
@pytest.mark.parametrize("buffer", (True, False)) @pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(buffer): def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer: if buffer:

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
import pytest import pytest
@ -10,7 +11,7 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_
test_file = "Tests/images/hopper.psd" test_file = "Tests/images/hopper.psd"
def test_sanity(): def test_sanity() -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.load() im.load()
assert im.mode == "RGB" assert im.mode == "RGB"
@ -23,8 +24,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(): def test_unclosed_file() -> None:
def open(): def open() -> None:
im = Image.open(test_file) im = Image.open(test_file)
im.load() im.load()
@ -32,27 +33,27 @@ def test_unclosed_file():
open() open()
def test_closed_file(): def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
im = Image.open(test_file) im = Image.open(test_file)
im.load() im.load()
im.close() im.close()
def test_context_manager(): def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.load() im.load()
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
PsdImagePlugin.PsdImageFile(invalid_file) PsdImagePlugin.PsdImageFile(invalid_file)
def test_n_frames(): def test_n_frames() -> None:
with Image.open("Tests/images/hopper_merged.psd") as im: with Image.open("Tests/images/hopper_merged.psd") as im:
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
@ -63,7 +64,7 @@ def test_n_frames():
assert im.is_animated assert im.is_animated
def test_eoferror(): def test_eoferror() -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
# PSD seek index starts at 1 rather than 0 # PSD seek index starts at 1 rather than 0
n_frames = im.n_frames + 1 n_frames = im.n_frames + 1
@ -77,7 +78,7 @@ def test_eoferror():
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_seek_tell(): def test_seek_tell() -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
layer_number = im.tell() layer_number = im.tell()
assert layer_number == 1 assert layer_number == 1
@ -94,30 +95,30 @@ def test_seek_tell():
assert layer_number == 2 assert layer_number == 2
def test_seek_eoferror(): def test_seek_eoferror() -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
with pytest.raises(EOFError): with pytest.raises(EOFError):
im.seek(-1) im.seek(-1)
def test_open_after_exclusive_load(): def test_open_after_exclusive_load() -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.load() im.load()
im.seek(im.tell() + 1) im.seek(im.tell() + 1)
im.load() im.load()
def test_rgba(): def test_rgba() -> None:
with Image.open("Tests/images/rgba.psd") as im: with Image.open("Tests/images/rgba.psd") as im:
assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") 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: with Image.open("Tests/images/five_channels.psd") as im:
assert im.n_frames == 1 assert im.n_frames == 1
def test_icc_profile(): def test_icc_profile() -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert "icc_profile" in im.info assert "icc_profile" in im.info
@ -125,12 +126,12 @@ def test_icc_profile():
assert len(icc_profile) == 3144 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: with Image.open("Tests/images/hopper_merged.psd") as im:
assert "icc_profile" not in im.info 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 # The combined size of the individual parts is larger than the
# declared 'size' of the extra data field, resulting in a backwards seek. # 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), ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
], ],
) )
def test_crashes(test_file, raises): def test_crashes(test_file, raises) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
with pytest.raises(raises): with pytest.raises(raises):
with Image.open(f): with Image.open(f):

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, QoiImagePlugin from PIL import Image, QoiImagePlugin
@ -6,7 +7,7 @@ from PIL import Image, QoiImagePlugin
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
def test_sanity(): def test_sanity() -> None:
with Image.open("Tests/images/hopper.qoi") as im: with Image.open("Tests/images/hopper.qoi") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (128, 128) assert im.size == (128, 128)
@ -22,7 +23,7 @@ def test_sanity():
assert_image_equal_tofile(im, "Tests/images/pil123rgba.png") assert_image_equal_tofile(im, "Tests/images/pil123rgba.png")
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):

View File

@ -1,4 +1,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import Image, SgiImagePlugin from PIL import Image, SgiImagePlugin
@ -11,7 +14,7 @@ from .helper import (
) )
def test_rgb(): def test_rgb() -> None:
# Created with ImageMagick then renamed: # Created with ImageMagick then renamed:
# convert hopper.ppm -compress None sgi:hopper.rgb # convert hopper.ppm -compress None sgi:hopper.rgb
test_file = "Tests/images/hopper.rgb" test_file = "Tests/images/hopper.rgb"
@ -21,11 +24,11 @@ def test_rgb():
assert im.get_format_mimetype() == "image/rgb" assert im.get_format_mimetype() == "image/rgb"
def test_rgb16(): def test_rgb16() -> None:
assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb") assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb")
def test_l(): def test_l() -> None:
# Created with ImageMagick # Created with ImageMagick
# convert hopper.ppm -monochrome -compress None sgi:hopper.bw # convert hopper.ppm -monochrome -compress None sgi:hopper.bw
test_file = "Tests/images/hopper.bw" test_file = "Tests/images/hopper.bw"
@ -35,7 +38,7 @@ def test_l():
assert im.get_format_mimetype() == "image/sgi" assert im.get_format_mimetype() == "image/sgi"
def test_rgba(): def test_rgba() -> None:
# Created with ImageMagick: # Created with ImageMagick:
# convert transparent.png -compress None transparent.sgi # convert transparent.png -compress None transparent.sgi
test_file = "Tests/images/transparent.sgi" test_file = "Tests/images/transparent.sgi"
@ -45,7 +48,7 @@ def test_rgba():
assert im.get_format_mimetype() == "image/sgi" assert im.get_format_mimetype() == "image/sgi"
def test_rle(): def test_rle() -> None:
# Created with ImageMagick: # Created with ImageMagick:
# convert hopper.ppm hopper.sgi # convert hopper.ppm hopper.sgi
test_file = "Tests/images/hopper.sgi" test_file = "Tests/images/hopper.sgi"
@ -54,22 +57,22 @@ def test_rle():
assert_image_equal_tofile(im, "Tests/images/hopper.rgb") assert_image_equal_tofile(im, "Tests/images/hopper.rgb")
def test_rle16(): def test_rle16() -> None:
test_file = "Tests/images/tv16.sgi" test_file = "Tests/images/tv16.sgi"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert_image_equal_tofile(im, "Tests/images/tv.rgb") assert_image_equal_tofile(im, "Tests/images/tv.rgb")
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(ValueError): with pytest.raises(ValueError):
SgiImagePlugin.SgiImageFile(invalid_file) SgiImagePlugin.SgiImageFile(invalid_file)
def test_write(tmp_path): def test_write(tmp_path: Path) -> None:
def roundtrip(img): def roundtrip(img: Image.Image) -> None:
out = str(tmp_path / "temp.sgi") out = str(tmp_path / "temp.sgi")
img.save(out, format="sgi") img.save(out, format="sgi")
assert_image_equal_tofile(img, out) assert_image_equal_tofile(img, out)
@ -88,7 +91,7 @@ def test_write(tmp_path):
roundtrip(Image.new("L", (10, 1))) roundtrip(Image.new("L", (10, 1)))
def test_write16(tmp_path): def test_write16(tmp_path: Path) -> None:
test_file = "Tests/images/hopper16.rgb" test_file = "Tests/images/hopper16.rgb"
with Image.open(test_file) as im: with Image.open(test_file) as im:
@ -98,7 +101,7 @@ def test_write16(tmp_path):
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
def test_unsupported_mode(tmp_path): def test_unsupported_mode(tmp_path: Path) -> None:
im = hopper("LA") im = hopper("LA")
out = str(tmp_path / "temp.sgi") out = str(tmp_path / "temp.sgi")

View File

@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
import tempfile import tempfile
import warnings import warnings
from io import BytesIO from io import BytesIO
from pathlib import Path
import pytest import pytest
@ -12,7 +14,7 @@ from .helper import assert_image_equal_tofile, hopper, is_pypy
TEST_FILE = "Tests/images/hopper.spider" TEST_FILE = "Tests/images/hopper.spider"
def test_sanity(): def test_sanity() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()
assert im.mode == "F" assert im.mode == "F"
@ -21,8 +23,8 @@ def test_sanity():
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(): def test_unclosed_file() -> None:
def open(): def open() -> None:
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
@ -30,20 +32,20 @@ def test_unclosed_file():
open() open()
def test_closed_file(): def test_closed_file() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
im.close() im.close()
def test_context_manager(): def test_context_manager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()
def test_save(tmp_path): def test_save(tmp_path: Path) -> None:
# Arrange # Arrange
temp = str(tmp_path / "temp.spider") temp = str(tmp_path / "temp.spider")
im = hopper() im = hopper()
@ -58,7 +60,7 @@ def test_save(tmp_path):
assert im2.format == "SPIDER" assert im2.format == "SPIDER"
def test_tempfile(): def test_tempfile() -> None:
# Arrange # Arrange
im = hopper() im = hopper()
@ -74,11 +76,11 @@ def test_tempfile():
assert reloaded.format == "SPIDER" assert reloaded.format == "SPIDER"
def test_is_spider_image(): def test_is_spider_image() -> None:
assert SpiderImagePlugin.isSpiderImage(TEST_FILE) assert SpiderImagePlugin.isSpiderImage(TEST_FILE)
def test_tell(): def test_tell() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act # Act
@ -88,13 +90,13 @@ def test_tell():
assert index == 0 assert index == 0
def test_n_frames(): def test_n_frames() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
def test_load_image_series(): def test_load_image_series() -> None:
# Arrange # Arrange
not_spider_file = "Tests/images/hopper.ppm" not_spider_file = "Tests/images/hopper.ppm"
file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] 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) assert img_list[0].size == (128, 128)
def test_load_image_series_no_input(): def test_load_image_series_no_input() -> None:
# Arrange # Arrange
file_list = None file_list = None
@ -119,7 +121,7 @@ def test_load_image_series_no_input():
assert img_list is None assert img_list is None
def test_is_int_not_a_number(): def test_is_int_not_a_number() -> None:
# Arrange # Arrange
not_a_number = "a" not_a_number = "a"
@ -130,7 +132,7 @@ def test_is_int_not_a_number():
assert ret == 0 assert ret == 0
def test_invalid_file(): def test_invalid_file() -> None:
invalid_file = "Tests/images/invalid.spider" invalid_file = "Tests/images/invalid.spider"
with pytest.raises(OSError): with pytest.raises(OSError):
@ -138,20 +140,20 @@ def test_invalid_file():
pass pass
def test_nonstack_file(): def test_nonstack_file() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
with pytest.raises(EOFError): with pytest.raises(EOFError):
im.seek(0) im.seek(0)
def test_nonstack_dos(): def test_nonstack_dos() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
for i, frame in enumerate(ImageSequence.Iterator(im)): for i, frame in enumerate(ImageSequence.Iterator(im)):
assert i <= 1, "Non-stack DOS file test failed" assert i <= 1, "Non-stack DOS file test failed"
# for issue #4093 # for issue #4093
def test_odd_size(): def test_odd_size() -> None:
data = BytesIO() data = BytesIO()
width = 100 width = 100
im = Image.new("F", (width, 64)) im = Image.new("F", (width, 64))

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os import os
import pytest import pytest
@ -10,7 +11,7 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper
EXTRA_DIR = "Tests/images/sunraster" EXTRA_DIR = "Tests/images/sunraster"
def test_sanity(): def test_sanity() -> None:
# Arrange # Arrange
# Created with ImageMagick: convert hopper.jpg hopper.ras # Created with ImageMagick: convert hopper.jpg hopper.ras
test_file = "Tests/images/hopper.ras" test_file = "Tests/images/hopper.ras"
@ -27,7 +28,7 @@ def test_sanity():
SunImagePlugin.SunImageFile(invalid_file) SunImagePlugin.SunImageFile(invalid_file)
def test_im1(): def test_im1() -> None:
with Image.open("Tests/images/sunraster.im1") as im: with Image.open("Tests/images/sunraster.im1") as im:
assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png")
@ -35,7 +36,7 @@ def test_im1():
@pytest.mark.skipif( @pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
) )
def test_others(): def test_others() -> None:
files = ( files = (
os.path.join(EXTRA_DIR, f) os.path.join(EXTRA_DIR, f)
for f in os.listdir(EXTRA_DIR) for f in os.listdir(EXTRA_DIR)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
import pytest import pytest
@ -18,7 +19,7 @@ TEST_TAR_FILE = "Tests/images/hopper.tar"
("jpg", "hopper.jpg", "JPEG"), ("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): if features.check(codec):
with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar:
with Image.open(tar) as im: 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") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(): def test_unclosed_file() -> None:
with pytest.warns(ResourceWarning): with pytest.warns(ResourceWarning):
TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
def test_close(): def test_close() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
tar.close() tar.close()
def test_contextmanager(): def test_contextmanager() -> None:
with warnings.catch_warnings(): with warnings.catch_warnings():
with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"):
pass pass

Some files were not shown because too many files have changed in this diff Show More