Merge branch 'main' into cross-prefix

This commit is contained in:
James Le Cuirot 2024-02-24 11:00:45 +00:00 committed by GitHub
commit ceaf4496fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
338 changed files with 5394 additions and 3488 deletions

View File

@ -6,6 +6,7 @@ init:
# Uncomment previous line to get RDP access during the build. # Uncomment previous line to get RDP access during the build.
environment: environment:
COVERAGE_CORE: sysmon
EXECUTABLE: python.exe EXECUTABLE: python.exe
TEST_OPTIONS: TEST_OPTIONS:
DEPLOY: YES DEPLOY: YES
@ -14,7 +15,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

@ -0,0 +1 @@
mypy==1.7.1

View File

@ -2,15 +2,19 @@
[report] [report]
# Regexes for lines to exclude from consideration # Regexes for lines to exclude from consideration
exclude_lines = exclude_also =
# Have to re-enable the standard pragma: # Don't complain if non-runnable code isn't run
pragma: no cover
# Don't complain if non-runnable code isn't run:
if 0: if 0:
if __name__ == .__main__.: if __name__ == .__main__.:
# Don't complain about debug code # Don't complain about debug code
if DEBUG: if DEBUG:
# Don't complain about compatibility code for missing optional dependencies
except ImportError
if TYPE_CHECKING:
@abc.abstractmethod
# Empty bodies in protocols or abstract methods
^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$
^\s*\.\.\.(\s*#.*)?$
[run] [run]
omit = omit =

6
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,6 @@
# Flake8
8de95676e0fd89f2326b3953488ab66ff29cd2d0
# Format with Black
53a7e3500437a9fd5826bc04758f7116bd7e52dc
# Format the C code with ClangFormat
46b7e86bab79450ec0a2866c6c0c679afb659d17

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
tidelift: "pypi/Pillow" tidelift: "pypi/pillow"

18
.github/problem-matchers/gcc.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"__comment": "Based on vscode-cpptools' Extension/package.json gcc rule",
"problemMatcher": [
{
"owner": "gcc-problem-matcher",
"pattern": [
{
"regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

View File

@ -13,6 +13,8 @@ categories:
label: "Removal" label: "Removal"
- title: "Testing" - title: "Testing"
label: "Testing" label: "Testing"
- title: "Type hints"
label: "Type hints"
exclude-labels: exclude-labels:
- "changelog: skip" - "changelog: skip"

View File

@ -7,10 +7,12 @@ on:
paths: paths:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- "docs/**" - "docs/**"
- "src/PIL/**"
pull_request: pull_request:
paths: paths:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- "docs/**" - "docs/**"
- "src/PIL/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -37,16 +39,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:
@ -28,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
@ -49,9 +50,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 +71,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 +83,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 +98,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 +144,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:
@ -28,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
@ -84,7 +85,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:
@ -26,13 +26,16 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"]
timeout-minutes: 30 timeout-minutes: 30
@ -66,8 +69,16 @@ jobs:
- name: Print build system information - name: Print build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma - name: Install Python dependencies
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma run: >
python3 -m pip install
coverage>=7.4.2
defusedxml
olefile
pyroma
pytest
pytest-cov
pytest-timeout
- name: Install dependencies - name: Install dependencies
id: install id: install
@ -89,7 +100,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 +213,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,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs: jobs:
build: build:
@ -35,7 +37,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 +51,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 +79,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')
@ -86,6 +109,10 @@ jobs:
env: env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build - name: Build
run: | run: |
.ci/build.sh .ci/build.sh
@ -123,9 +150,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

@ -30,7 +30,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:
@ -39,18 +96,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:
@ -62,12 +119,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 }}
@ -75,24 +135,21 @@ jobs:
CIBW_TEST_SKIP: "*-macosx_arm64" CIBW_TEST_SKIP: "*-macosx_arm64"
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
with: with:
name: dist 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
@ -106,6 +163,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
@ -114,12 +175,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 }}
# Cannot cross-compile FriBiDi (only used for tests)
$FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}")
if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" }
& python.exe winbuild\build_prepare.py -v @FLAGS
shell: pwsh shell: pwsh
- name: Build wheels - name: Build wheels
@ -146,6 +202,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw" CIBW_CACHE_PATH: "C:\\cibw"
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow -v {project}:C:\pillow
@ -157,24 +214,16 @@ jobs:
shell: cmd shell: cmd
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: dist name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
- name: Prepare to upload FriBiDi
if: "matrix.arch != 'ARM64'"
run: |
mkdir fribidi\${{ matrix.arch }}
copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }}
shell: cmd
- name: Upload fribidi.dll - name: Upload fribidi.dll
if: "matrix.arch != 'ARM64'" uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with: with:
name: fribidi name: fribidi-windows-${{ matrix.cibw_arch }}
path: fribidi\* path: winbuild\build\bin\fribidi*
sdist: sdist:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -190,17 +239,26 @@ jobs:
- run: make sdist - run: make sdist
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v4
with: with:
name: dist name: dist-sdist
path: dist/*.tar.gz path: dist/*.tar.gz
success: pypi-publish:
permissions: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
contents: none needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
needs: [build, windows, sdist]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Wheels Successful name: Upload release to PyPI
environment:
name: release-pypi
url: https://pypi.org/p/Pillow
permissions:
id-token: write
steps: steps:
- name: Success - uses: actions/download-artifact@v4
run: echo Wheels Successful with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

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.7 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.0 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,9 +2,90 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
10.2.0 (unreleased) 10.3.0 (unreleased)
------------------- -------------------
- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782
[evanmiller, radarhere]
- Fixed reading FLI/FLC images with a prefix chunk #7804
[twolife]
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
[nik012003, radarhere]
- Remove execute bit from ``setup.py`` #7760
[hugovk]
- Do not support using test-image-results to upload images after test failures #7739
[radarhere]
- Changed ImageMath.ops to be static #7721
[radarhere]
- Fix APNG info after seeking backwards more than twice #7701
[esoma, radarhere]
- Deprecate ImageCms constants and versions() function #7702
[nulano, radarhere]
- Added PerspectiveTransform #7699
[radarhere]
- Add support for reading and writing grayscale PFM images #7696
[nulano, hugovk]
- Add LCMS2 flags to ImageCms #7676
[nulano, radarhere, hugovk]
- Rename x64 to AMD64 in winbuild #7693
[nulano]
10.2.0 (2024-01-02)
-------------------
- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553
[bgilbert, radarhere]
- Trim glyph size in ImageFont.getmask() #7669, #7672
[radarhere, nulano]
- Deprecate IptcImagePlugin helpers #7664
[nulano, hugovk, radarhere]
- Allow uncompressed TIFF images to be saved in chunks #7650
[radarhere]
- Concatenate multiple JPEG EXIF markers #7496
[radarhere]
- Changed IPTC tile tuple to match other plugins #7661
[radarhere]
- Do not assign new fp attribute when exiting context manager #7566
[radarhere]
- Support arbitrary masks for uncompressed RGB DDS images #7589
[radarhere, akx]
- Support setting ROWSPERSTRIP tag #7654
[radarhere]
- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662
[radarhere]
- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657
[hugovk]
- Restricted environment keys for ImageMath.eval() #7655
[wiredfool, radarhere]
- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641
[hugovk, radarhere]
- Fix incorrect color blending for overlapping glyphs #7497
[ZachNagengast, nulano, radarhere]
- Attempt memory mapping when tile args is a string #7565 - Attempt memory mapping when tile args is a string #7565
[radarhere] [radarhere]

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is Pillow is the friendly PIL fork. It is
Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors. Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
Like PIL, Pillow is licensed under the open source HPND License: Like PIL, Pillow is licensed under the open source HPND License:

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>
@ -67,11 +64,11 @@ As of 2019, Pillow development is
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a> src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a>
<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.
@ -20,12 +20,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
git tag 5.2.0 git tag 5.2.0
git push --tags git push --tags
``` ```
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check and upload all source and binary distributions e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.0*
```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
@ -55,12 +50,7 @@ Released as needed for security, installation or critical bug fixes.
```bash ```bash
make sdist make sdist
``` ```
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check and upload all source and binary distributions e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.1*
```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash ```bash
git push git push
@ -82,11 +72,7 @@ Released as needed privately to individual vendors for critical security-related
git tag 2.5.3 git tag 2.5.3
git push origin --tags git push origin --tags
``` ```
* [ ] Create and check source distribution: * [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
```bash
make sdist
```
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash ```bash
git push origin 2.5.x git push origin 2.5.x
@ -94,14 +80,9 @@ Released as needed privately to individual vendors for critical security-related
## Source and Binary Distributions ## Source and Binary Distributions
* [ ] Download sdist and wheels from 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)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): has passed, including the "Upload release to PyPI" job. This will have been triggered
```bash by the new tag.
gh run download --dir dist
# select dist
```
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
and copy into `dist`.
## 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

Binary file not shown.

Binary file not shown.

View File

@ -2,7 +2,6 @@
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSans-Regular.ttf, from https://www.google.com/get/noto/ NotoSans-Regular.ttf, from https://www.google.com/get/noto/
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
@ -25,3 +24,5 @@ FreeMono.ttf is licensed under GPLv3.
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base 10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
"Public domain font. Share and enjoy." "Public domain font. Share and enjoy."
CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain.

Binary file not shown.

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

BIN
Tests/images/2422.flc Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

BIN
Tests/images/bgr15.dds Normal file

Binary file not shown.

BIN
Tests/images/bgr15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
Tests/images/cbdt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

BIN
Tests/images/cbdt_mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
Tests/images/hopper.pfm Normal file

Binary file not shown.

BIN
Tests/images/hopper_be.pfm Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -13,7 +13,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from __future__ import annotations
import atheris import atheris
@ -24,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:
@ -33,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");
@ -13,7 +11,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from __future__ import annotations
import atheris import atheris
@ -24,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:
@ -33,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: int | None, expected: str) -> 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: str, plural: bool, expected: str) -> 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: str) -> 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,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import io import io
import re import re
from typing import Callable
import pytest import pytest
@ -14,7 +16,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 +26,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: str, function: Callable[[str], bool]) -> 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 +48,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: str) -> 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: str) -> 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 +107,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 +117,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: 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,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: Image.Image) -> 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: str, length: int) -> 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
@ -32,6 +35,7 @@ TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SR
TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds" TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
@ -44,7 +48,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
TEST_FILE_DX10_BC1_TYPELESS, 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")
@ -58,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:
@ -71,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:
@ -92,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:
@ -113,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:
@ -134,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:
@ -158,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:
@ -172,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:
@ -185,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:
@ -198,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:
@ -214,7 +218,7 @@ def test_dx10_bc7_unorm_srgb():
) )
def test_dx10_r8g8b8a8(): def test_dx10_r8g8b8a8() -> None:
"""Check DX10 images can be opened""" """Check DX10 images can be opened"""
with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: with Image.open(TEST_FILE_DX10_R8G8B8A8) as im:
@ -227,7 +231,7 @@ def test_dx10_r8g8b8a8():
assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) 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:
@ -249,10 +253,11 @@ def test_dx10_r8g8b8a8_unorm_srgb():
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB), ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15),
("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:
@ -263,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"
@ -275,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"
@ -287,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
@ -307,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()
@ -321,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:
@ -336,21 +341,14 @@ 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")
@pytest.mark.parametrize( def test_unsupported_bitcount() -> None:
"test_file",
(
"Tests/images/unsupported_bitcount_rgb.dds",
"Tests/images/unsupported_bitcount_luminance.dds",
),
)
def test_unsupported_bitcount(test_file):
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open(test_file): with Image.open("Tests/images/unsupported_bitcount.dds"):
pass pass
@ -361,13 +359,13 @@ def test_unsupported_bitcount(test_file):
"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):
@ -383,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,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
import pytest import pytest
from PIL import FliImagePlugin, Image from PIL import FliImagePlugin, Image, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
@ -11,11 +12,14 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
# save as...-> hopper.fli, default options. # save as...-> hopper.fli, default options.
static_test_file = "Tests/images/hopper.fli" static_test_file = "Tests/images/hopper.fli"
# From https://samples.libav.org/fli-flc/ # From https://samples.ffmpeg.org/fli-flc/
animated_test_file = "Tests/images/a.fli" animated_test_file = "Tests/images/a.fli"
# From https://samples.ffmpeg.org/fli-flc/
animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
def test_sanity():
def test_sanity() -> None:
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
im.load() im.load()
assert im.mode == "P" assert im.mode == "P"
@ -31,9 +35,27 @@ def test_sanity():
assert im.is_animated assert im.is_animated
def test_prefix_chunk() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
with Image.open(animated_test_file_with_prefix_chunk) as im:
assert im.mode == "P"
assert im.size == (320, 200)
assert im.format == "FLI"
assert im.info["duration"] == 171
assert im.is_animated
palette = im.getpalette()
assert palette[3:6] == [255, 255, 255]
assert palette[381:384] == [204, 204, 12]
assert palette[765:] == [252, 0, 0]
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @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 +63,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 +79,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 +95,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 +118,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 +131,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 +153,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 +168,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 +181,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)

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