Merge branch 'main' into buffer-updates

This commit is contained in:
Andrew Murray 2024-07-19 21:56:16 +10:00 committed by GitHub
commit 849ffd6075
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
299 changed files with 6928 additions and 3896 deletions

View File

@ -21,9 +21,9 @@ environment:
- PYTHON: C:/Python312 - PYTHON: C:/Python312
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64 - PYTHON: C:/Python39-x64
ARCHITECTURE: AMD64 ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
install: install:
@ -32,13 +32,13 @@ install:
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-test-images.zip -oc:\ - 7z x pillow-test-images.zip -oc:\
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip - curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
- 7z x nasm-win64.zip -oc:\ - 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.3.0 - choco install ghostscript --version=10.3.1
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% - path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - ps: |
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
c:\pillow\winbuild\build\build_dep_all.cmd c:\pillow\winbuild\build\build_dep_all.cmd
$host.SetShouldExit(0) $host.SetShouldExit(0)
- path C:\pillow\winbuild\build\bin;%PATH% - path C:\pillow\winbuild\build\bin;%PATH%

View File

@ -28,8 +28,6 @@ fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
# TODO Update condition when cffi supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile
@ -39,19 +37,23 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
# TODO Update condition when NumPy supports 3.13 # TODO Update condition when NumPy supports free-threading
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi if [[ "$PYTHON_GIL" == "0" ]]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6 # TODO Update condition when pyqt6 supports free-threading
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
fi fi
# Pyroma uses non-isolated build and fails with old setuptools # Pyroma uses non-isolated build and fails with old setuptools
if [[ if [[
$GHA_PYTHON_VERSION == pypy3.9 $GHA_PYTHON_VERSION == pypy3.9
|| $GHA_PYTHON_VERSION == 3.8
|| $GHA_PYTHON_VERSION == 3.9 || $GHA_PYTHON_VERSION == 3.9
]]; then ]]; then
# To match pyproject.toml # To match pyproject.toml

View File

@ -1 +1 @@
cibuildwheel==2.18.1 cibuildwheel==2.19.2

View File

@ -1 +1 @@
mypy==1.10.0 mypy==1.10.1

View File

@ -3,7 +3,7 @@
BasedOnStyle: Google BasedOnStyle: Google
AlwaysBreakAfterReturnType: All AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false
AlignAfterOpenBracket: AlwaysBreak AlignAfterOpenBracket: BlockIndent
BinPackArguments: false BinPackArguments: false
BinPackParameters: false BinPackParameters: false
BreakBeforeBraces: Attach BreakBeforeBraces: Attach

View File

@ -19,6 +19,5 @@ exclude_also =
[run] [run]
omit = omit =
Tests/32bit_segfault_check.py Tests/32bit_segfault_check.py
Tests/bench_cffi_access.py
Tests/check_*.py Tests/check_*.py
Tests/createfontdatachunk.py Tests/createfontdatachunk.py

View File

@ -24,6 +24,8 @@ concurrency:
jobs: jobs:
Fuzzing: Fuzzing:
# Disabled until google/oss-fuzz#11419 upgrades Python to 3.9+
if: false
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Build Fuzzers - name: Build Fuzzers

View File

@ -7,16 +7,17 @@ brew install \
ghostscript \ ghostscript \
libimagequant \ libimagequant \
libjpeg \ libjpeg \
libraqm \
libtiff \ libtiff \
little-cms2 \ little-cms2 \
openjpeg \ openjpeg \
webp webp
if [[ "$ImageOS" == "macos13" ]]; then
brew install --ignore-dependencies libraqm
else
brew install libraqm
fi
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
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile
@ -24,9 +25,7 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install numpy
# TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -35,7 +35,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-minor-version: [8, 9] python-minor-version: [9]
timeout-minutes: 40 timeout-minutes: 40
@ -72,7 +72,6 @@ jobs:
make make
netpbm netpbm
perl perl
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
python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-numpy

View File

@ -44,13 +44,11 @@ jobs:
amazon-2023-amd64, amazon-2023-amd64,
arch, arch,
centos-stream-9-amd64, centos-stream-9-amd64,
debian-11-bullseye-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-39-amd64, fedora-39-amd64,
fedora-40-amd64, fedora-40-amd64,
gentoo, gentoo,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64, ubuntu-24.04-noble-amd64,
] ]

View File

@ -64,7 +64,6 @@ jobs:
mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python3-cffi \
mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python3-setuptools \ mingw-w64-x86_64-python3-setuptools \

View File

@ -35,7 +35,7 @@ jobs:
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.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30 timeout-minutes: 30
@ -86,7 +86,7 @@ jobs:
choco install nasm --no-progress choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.3.0 --no-progress choco install ghostscript --version=10.3.1 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images # Install extra test images

View File

@ -48,33 +48,26 @@ jobs:
"3.11", "3.11",
"3.10", "3.10",
"3.9", "3.9",
"3.8",
] ]
include: include:
- python-version: "3.11" - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
PYTHONOPTIMIZE: 1 - { python-version: "3.10", PYTHONOPTIMIZE: 2 }
REVERSE: "--reverse" # Free-threaded
- python-version: "3.10" - { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true }
PYTHONOPTIMIZE: 2
# M1 only available for 3.10+ # M1 only available for 3.10+
- os: "macos-13" - { os: "macos-13", python-version: "3.9" }
python-version: "3.9"
- os: "macos-13"
python-version: "3.8"
exclude: exclude:
- os: "macos-14" - { os: "macos-14", python-version: "3.9" }
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 }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
if: "${{ !matrix.disable-gil }}"
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
@ -83,6 +76,18 @@ jobs:
".ci/*.sh" ".ci/*.sh"
"pyproject.toml" "pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.1.0
if: "${{ matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
nogil: ${{ matrix.disable-gil }}
- name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}"
run: |
echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py

View File

@ -16,9 +16,9 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds # Package versions for fresh source builds
FREETYPE_VERSION=2.13.2 FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.4.0 HARFBUZZ_VERSION=8.5.0
LIBPNG_VERSION=1.6.43 LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.2 JPEGTURBO_VERSION=3.0.3
OPENJPEG_VERSION=2.5.2 OPENJPEG_VERSION=2.5.2
XZ_VERSION=5.4.5 XZ_VERSION=5.4.5
TIFF_VERSION=4.6.0 TIFF_VERSION=4.6.0
@ -33,9 +33,9 @@ if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
else else
ZLIB_VERSION=1.2.8 ZLIB_VERSION=1.2.8
fi fi
LIBWEBP_VERSION=1.3.2 LIBWEBP_VERSION=1.4.0
BZIP2_VERSION=1.0.8 BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.16.1 LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0 BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
@ -70,7 +70,7 @@ function build {
fi fi
build_new_zlib build_new_zlib
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then if [ -n "$IS_MACOS" ]; then
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto build_simple xorgproto 2024.1 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

View File

@ -12,8 +12,14 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
else else
yum install -y fribidi yum install -y fribidi
fi fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
# TODO Update condition when NumPy supports free-threading
if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy python3 -m pip install numpy
fi
fi fi
if [ ! -d "test-images-main" ]; then if [ ! -d "test-images-main" ]; then

View File

@ -1,6 +1,14 @@
name: Wheels name: Wheels
on: on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
- cron: "42 1 * * 0,3"
push: push:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
@ -33,6 +41,7 @@ env:
jobs: jobs:
build-1-QEMU-emulated-wheels: build-1-QEMU-emulated-wheels:
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -41,11 +50,8 @@ jobs:
python-version: python-version:
- pp39 - pp39
- pp310 - pp310
- cp38 - cp3{9,10,11}
- cp39 - cp3{12,13}
- cp310
- cp311
- cp312
spec: spec:
- manylinux2014 - manylinux2014
- manylinux_2_28 - manylinux_2_28
@ -80,6 +86,7 @@ jobs:
CIBW_ARCHS: "aarch64" CIBW_ARCHS: "aarch64"
# Likewise, select only one Python version per job to speed this up. # Likewise, select only one Python version per job to speed this up.
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
CIBW_PRERELEASE_PYTHONS: True
# Extra options for manylinux. # Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
@ -131,10 +138,10 @@ jobs:
env: env:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }} CIBW_BUILD: ${{ matrix.build }}
CIBW_FREE_THREADED_SUPPORT: True
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 }}
CIBW_SKIP: pp38-* CIBW_PRERELEASE_PYTHONS: True
CIBW_TEST_SKIP: cp38-macosx_arm64
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@ -204,7 +211,8 @@ 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_FREE_THREADED_SUPPORT: True
CIBW_PRERELEASE_PYTHONS: True
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
@ -228,6 +236,7 @@ jobs:
path: winbuild\build\bin\fribidi* path: winbuild\build\bin\fribidi*
sdist: sdist:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -246,6 +255,23 @@ jobs:
name: dist-sdist name: dist-sdist
path: dist/*.tar.gz path: dist/*.tar.gz
scientific-python-nightly-wheels-publish:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
needs: [build-2-native-wheels, windows]
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v4
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
pypi-publish: pypi-publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.3 rev: v0.5.0
hooks: hooks:
- id: ruff - id: ruff
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
@ -11,7 +11,7 @@ repos:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.8 rev: 1.7.9
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.4 rev: v18.1.8
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -50,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.2 rev: 0.28.6
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
@ -62,12 +62,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.8.0 rev: 2.1.3
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.16 rev: v0.18
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject

View File

@ -3,7 +3,7 @@ version: 2
formats: [pdf] formats: [pdf]
build: build:
os: ubuntu-22.04 os: ubuntu-lts-latest
tools: tools:
python: "3" python: "3"
jobs: jobs:

View File

@ -2,9 +2,126 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
10.4.0 (unreleased) 11.0.0 (unreleased)
------------------- -------------------
- Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242
[radarhere]
- Changed ContainerIO to subclass IO #8240
[radarhere]
- Move away from APIs that use borrowed references under the free-threaded build #8216
[hugovk, lysnikolaou]
- Allow size argument to resize() to be a NumPy array #8201
[radarhere]
- Drop support for Python 3.8 #8183
[hugovk, radarhere]
- Add support for Python 3.13 #8181
[hugovk, radarhere]
- Fix incompatibility with NumPy 1.20 #8187
[neutrinoceros, radarhere]
- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
[hugovk, radarhere]
10.4.0 (2024-07-01)
-------------------
- Raise FileNotFoundError if show_file() path does not exist #8178
[radarhere]
- Improved reading 16-bit TGA images with colour #7965
[Yay295, radarhere]
- Deprecate non-image ImageCms modes #8031
[radarhere]
- Fixed processing multiple JPEG EXIF markers #8127
[radarhere]
- Do not preserve EXIFIFD tag by default when saving TIFF images #8110
[radarhere]
- Added ImageFont.load_default_imagefont() #8086
[radarhere]
- Added Image.WARN_POSSIBLE_FORMATS #8063
[radarhere]
- Remove zero-byte end padding when parsing any XMP data #8171
[radarhere]
- Do not detect Ultra HDR images as MPO #8056
[radarhere]
- Raise SyntaxError specific to JP2 #8146
[Yay295, radarhere]
- Do not use first frame duration for other frames when saving APNG images #8104
[radarhere]
- Consider I;16 pixel size when using a 1 mode mask #8112
[radarhere]
- When saving multiple PNG frames, convert to mode rather than raw mode #8087
[radarhere]
- Added byte support to FreeTypeFont #8141
[radarhere]
- Allow float center for rotate operations #8114
[radarhere]
- Do not read layers immediately when opening PSD images #8039
[radarhere]
- Restore original thread state #8065
[radarhere]
- Read IM and TIFF images as RGB, rather than RGBX #7997
[radarhere]
- Only preserve TIFF IPTC_NAA_CHUNK tag if type is BYTE or UNDEFINED #7948
[radarhere]
- Clarify ImageDraw2 error message when size is missing #8165
[radarhere]
- Support unpacking more rawmodes to RGBA palettes #7966
[radarhere]
- Removed support for Qt 5 #8159
[radarhere]
- Improve ``ImageFont.freetype`` support for XDG directories on Linux #8135
[mamg22, radarhere]
- Improved consistency of XMP handling #8069
[radarhere]
- Use pkg-config to help find libwebp and raqm #8142
[radarhere]
- Accept 't' suffix for libtiff version #8126, #8129
[radarhere]
- Deprecate ImageDraw.getdraw hints parameter #8124
[radarhere, hugovk]
- Added ImageDraw circle() #8085
[void4, hugovk, radarhere]
- Add mypy target to Makefile #8077
[Yay295]
- Added more modes to Image.MODES #7984
[radarhere]
- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978 - Deprecate BGR;15, BGR;16 and BGR;24 modes #7978
[radarhere, hugovk] [radarhere, hugovk]

View File

@ -118,3 +118,8 @@ lint-fix:
python3 -m black . python3 -m black .
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m ruff --fix . python3 -m ruff --fix .
.PHONY: mypy
mypy:
python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
python3 -m tox -e mypy

View File

@ -1,53 +0,0 @@
from __future__ import annotations
import time
from PIL import PyAccess
from .helper import hopper
# Not running this test by default. No DOS against CI.
def iterate_get(size, access) -> None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)]
def iterate_set(size, access) -> None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)] = (x % 256, y % 256, 0)
def timer(func, label, *args) -> None:
iterations = 5000
starttime = time.time()
for x in range(iterations):
func(*args)
if time.time() - starttime > 10:
break
endtime = time.time()
print(
f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, "
f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration"
)
def test_direct() -> None:
im = hopper()
im.load()
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
caccess = im.im.pixel_access(False)
access = PyAccess.new(im, False)
assert caccess[(0, 0)] == access[(0, 0)]
print(f"Size: {im.width}x{im.height}")
timer(iterate_get, "PyAccess - get", im.size, access)
timer(iterate_set, "PyAccess - set", im.size, access)
timer(iterate_get, "C-api - get", im.size, caccess)
timer(iterate_set, "C-api - set", im.size, caccess)

View File

@ -11,14 +11,15 @@ import subprocess
import sys import sys
import sysconfig import sysconfig
import tempfile import tempfile
from collections.abc import Sequence
from functools import lru_cache from functools import lru_cache
from io import BytesIO from io import BytesIO
from typing import Any, Callable, Sequence from typing import Any, Callable
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
from PIL import Image, ImageMath, features from PIL import Image, ImageFile, ImageMath, features
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -59,9 +60,7 @@ def convert_to_comparable(
return new_a, new_b return new_a, new_b
def assert_deep_equal( def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
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:
@ -174,12 +173,13 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
def skip_unless_feature_version( def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator: ) -> pytest.MarkDecorator:
if not features.check(feature): version = features.version(feature)
if version is None:
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 {required}" reason = f"{feature} is older than {required}"
version_required = parse_version(required) version_required = parse_version(required)
version_available = parse_version(features.version(feature)) version_available = parse_version(version)
return pytest.mark.skipif(version_available < version_required, reason=reason) return pytest.mark.skipif(version_available < version_required, reason=reason)
@ -189,12 +189,13 @@ def mark_if_feature_version(
version_blacklist: str, version_blacklist: str,
reason: str | None = None, reason: str | None = None,
) -> pytest.MarkDecorator: ) -> pytest.MarkDecorator:
if not features.check(feature): version = features.version(feature)
if version is None:
return pytest.mark.pil_noop_mark() return pytest.mark.pil_noop_mark()
if reason is None: if reason is None:
reason = f"{feature} is {version_blacklist}" reason = f"{feature} is {version_blacklist}"
version_required = parse_version(version_blacklist) version_required = parse_version(version_blacklist)
version_available = parse_version(features.version(feature)) version_available = parse_version(version)
if ( if (
version_available.major == version_required.major version_available.major == version_required.major
and version_available.minor == version_required.minor and version_available.minor == version_required.minor
@ -220,16 +221,11 @@ class PillowLeakTestCase:
from resource import RUSAGE_SELF, getrusage from resource import RUSAGE_SELF, getrusage
mem = getrusage(RUSAGE_SELF).ru_maxrss mem = getrusage(RUSAGE_SELF).ru_maxrss
if sys.platform == "darwin": # man 2 getrusage:
# man 2 getrusage: # ru_maxrss
# ru_maxrss # This is the maximum resident set size utilized
# This is the maximum resident set size utilized (in bytes). # in bytes on macOS, in kilobytes on Linux
return mem / 1024 # Kb return mem / 1024 if sys.platform == "darwin" else mem
# linux
# man 2 getrusage
# ru_maxrss (since Linux 2.6.32)
# This is the maximum resident set size used (in kilobytes).
return mem # Kb
def _test_leak(self, core: Callable[[], None]) -> None: def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage() start_mem = self._get_mem_usage()
@ -243,7 +239,7 @@ class PillowLeakTestCase:
# helpers # helpers
def fromstring(data: bytes) -> Image.Image: def fromstring(data: bytes) -> ImageFile.ImageFile:
return Image.open(BytesIO(data)) return Image.open(BytesIO(data))

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 414 B

BIN
Tests/images/rgba16.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

BIN
Tests/images/ultrahdr.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

View File

@ -12,8 +12,9 @@ from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"): if sys.platform.startswith("win32"):
pytest.skip("Fuzzer is linux only", allow_module_level=True) pytest.skip("Fuzzer is linux only", allow_module_level=True)
if features.check("libjpeg_turbo"): libjpeg_turbo_version = features.version("libjpeg_turbo")
version = packaging.version.parse(features.version("libjpeg_turbo")) if libjpeg_turbo_version is not None:
version = packaging.version.parse(libjpeg_turbo_version)
if version.major == 2 and version.minor == 0: if version.major == 2 and version.minor == 0:
pytestmark = pytest.mark.valgrind_known_error( pytestmark = pytest.mark.valgrind_known_error(
reason="Known failing with libjpeg_turbo 2.0" reason="Known failing with libjpeg_turbo 2.0"

View File

@ -321,6 +321,7 @@ class TestColorLut3DCoreAPI:
-1, 2, 2, 2, 2, 2, -1, 2, 2, 2, 2, 2,
])).load() ])).load()
# fmt: on # fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255) assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255) assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255) assert transformed[255, 0] == (0, 255, 255)
@ -341,6 +342,7 @@ class TestColorLut3DCoreAPI:
-3, 5, 5, 5, 5, 5, -3, 5, 5, 5, 5, 5,
])).load() ])).load()
# fmt: on # fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255) assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255) assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255) assert transformed[255, 0] == (0, 255, 255)
@ -354,10 +356,10 @@ class TestColorLut3DCoreAPI:
class TestColorLut3DFilter: class TestColorLut3DFilter:
def test_wrong_args(self) -> None: 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]) # type: ignore[arg-type]
with pytest.raises(ValueError, match="should be either an integer"): with pytest.raises(ValueError, match="should be either an integer"):
ImageFilter.Color3DLUT((11, 11), [1]) ImageFilter.Color3DLUT((11, 11), [1]) # type: ignore[arg-type]
with pytest.raises(ValueError, match=r"in \[2, 65\] range"): with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
ImageFilter.Color3DLUT((11, 11, 1), [1]) ImageFilter.Color3DLUT((11, 11, 1), [1])

View File

@ -12,7 +12,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb: class TestDecompressionBomb:
def teardown_method(self, method) -> None: def teardown_method(self) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def test_no_warning_small_file(self) -> None: def test_no_warning_small_file(self) -> None:

View File

@ -9,9 +9,9 @@ from PIL import _deprecate
"version, expected", "version, expected",
[ [
( (
11, 12,
"Old thing is deprecated and will be removed in Pillow 11 " "Old thing is deprecated and will be removed in Pillow 12 "
r"\(2024-10-15\)\. Use new thing instead\.", r"\(2025-10-15\)\. Use new thing instead\.",
), ),
( (
None, None,
@ -54,18 +54,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
def test_plural() -> None: 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 12 \(2025-10-15\)\. "
r"Use new thing instead\." r"Use new thing instead\."
) )
with pytest.warns(DeprecationWarning, match=expected): with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old things", 11, "new thing", plural=True) _deprecate.deprecate("Old things", 12, "new thing", plural=True)
def test_replacement_and_action() -> None: 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(
"Old thing", 11, replacement="new thing", action="Upgrade to new thing" "Old thing", 12, replacement="new thing", action="Upgrade to new thing"
) )
@ -78,16 +78,16 @@ def test_replacement_and_action() -> None:
) )
def test_action(action: str) -> None: 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 12 \(2025-10-15\)\. "
r"Upgrade to new thing\." r"Upgrade to new thing\."
) )
with pytest.warns(DeprecationWarning, match=expected): with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 11, action=action) _deprecate.deprecate("Old thing", 12, action=action)
def test_no_replacement_or_action() -> None: 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 12 \(2025-10-15\)"
) )
with pytest.warns(DeprecationWarning, match=expected): with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 11) _deprecate.deprecate("Old thing", 12)

View File

@ -30,7 +30,7 @@ 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: str, function: Callable[[str], bool]) -> None: def test(name: str, function: Callable[[str], str | None]) -> 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
@ -38,7 +38,9 @@ def test_version() -> None:
assert function(name) == version assert function(name) == version
if name != "PIL": if name != "PIL":
if name == "zlib" and version is not None: if name == "zlib" and version is not None:
version = version.replace(".zlib-ng", "") version = re.sub(".zlib-ng$", "", version)
elif name == "libtiff" and version is not None:
version = re.sub("t$", "", version)
assert version is None or re.search(r"\d+(\.\d+)*$", version) assert version is None or re.search(r"\d+(\.\d+)*$", version)
for module in features.modules: for module in features.modules:
@ -67,12 +69,16 @@ def test_webp_anim() -> None:
@skip_unless_feature("libjpeg_turbo") @skip_unless_feature("libjpeg_turbo")
def test_libjpeg_turbo_version() -> None: def test_libjpeg_turbo_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo")) version = features.version("libjpeg_turbo")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@skip_unless_feature("libimagequant") @skip_unless_feature("libimagequant")
def test_libimagequant_version() -> None: def test_libimagequant_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) version = features.version("libimagequant")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@pytest.mark.parametrize("feature", features.modules) @pytest.mark.parametrize("feature", features.modules)
@ -120,7 +126,7 @@ def test_unsupported_module() -> None:
@pytest.mark.parametrize("supported_formats", (True, False)) @pytest.mark.parametrize("supported_formats", (True, False))
def test_pilinfo(supported_formats) -> None: def test_pilinfo(supported_formats: bool) -> None:
buf = io.StringIO() buf = io.StringIO()
features.pilinfo(buf, supported_formats=supported_formats) features.pilinfo(buf, supported_formats=supported_formats)
out = buf.getvalue() out = buf.getvalue()

View File

@ -706,10 +706,21 @@ def test_different_modes_in_later_frames(
assert reloaded.mode == mode assert reloaded.mode == mode
def test_apng_repeated_seeks_give_correct_info() -> None: def test_different_durations(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/apng/different_durations.png") as im: with Image.open("Tests/images/apng/different_durations.png") as im:
for i in range(3): for _ in range(3):
im.seek(0) im.seek(0)
assert im.info["duration"] == 4000 assert im.info["duration"] == 4000
im.seek(1) im.seek(1)
assert im.info["duration"] == 1000 assert im.info["duration"] == 1000
im.save(test_file, save_all=True)
with Image.open(test_file) as reloaded:
assert reloaded.info["duration"] == 4000
reloaded.seek(1)
assert reloaded.info["duration"] == 1000

View File

@ -140,7 +140,7 @@ def test_load_dib() -> None:
(124, "g/pal8v5.bmp"), (124, "g/pal8v5.bmp"),
), ),
) )
def test_dib_header_size(header_size, path): def test_dib_header_size(header_size: int, path: str) -> None:
image_path = "Tests/images/bmp/" + path image_path = "Tests/images/bmp/" + path
with open(image_path, "rb") as fp: with open(image_path, "rb") as fp:
data = fp.read()[14:] data = fp.read()[14:]

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import IO
import pytest import pytest
from PIL import BufrStubImagePlugin, Image from PIL import BufrStubImagePlugin, Image, ImageFile
from .helper import hopper from .helper import hopper
@ -50,30 +51,33 @@ def test_save(tmp_path: Path) -> None:
def test_handler(tmp_path: Path) -> None: def test_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler(ImageFile.StubHandler):
opened = False opened = False
loaded = False loaded = False
saved = False saved = False
def open(self, im) -> None: def open(self, im: ImageFile.StubImageFile) -> None:
self.opened = True self.opened = True
def load(self, im): def load(self, im: ImageFile.StubImageFile) -> Image.Image:
self.loaded = True self.loaded = True
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) return Image.new("RGB", (1, 1))
def save(self, im, fp, filename) -> None: def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True self.saved = True
handler = TestHandler() handler = TestHandler()
BufrStubImagePlugin.register_handler(handler) BufrStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert handler.opened assert handler.opened
assert not handler.loaded assert not handler.is_loaded()
im.load() im.load()
assert handler.loaded assert handler.is_loaded()
temp_file = str(tmp_path / "temp.bufr") temp_file = str(tmp_path / "temp.bufr")
im.save(temp_file) im.save(temp_file)

View File

@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
from typing import Literal
import pytest import pytest
from PIL import ContainerIO, Image from PIL import ContainerIO, Image
@ -23,6 +21,13 @@ def test_isatty() -> None:
assert container.isatty() is False assert container.isatty() is False
def test_seekable() -> None:
with hopper() as im:
container = ContainerIO.ContainerIO(im, 0, 0)
assert container.seekable() is True
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mode, expected_position", "mode, expected_position",
( (
@ -31,7 +36,7 @@ def test_isatty() -> None:
(2, 100), (2, 100),
), ),
) )
def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: def test_seek_mode(mode: int, expected_position: int) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb") as fh: with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -44,6 +49,14 @@ def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None:
assert container.tell() == expected_position assert container.tell() == expected_position
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readable(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert container.readable() is True
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n0(bytesmode: bool) -> None: def test_read_n0(bytesmode: bool) -> None:
# Arrange # Arrange
@ -51,7 +64,7 @@ def test_read_n0(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
# Act # Act
container.seek(81) assert container.seek(81) == 81
data = container.read() data = container.read()
# Assert # Assert
@ -67,7 +80,7 @@ def test_read_n(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
# Act # Act
container.seek(81) assert container.seek(81) == 81
data = container.read(3) data = container.read(3)
# Assert # Assert
@ -83,7 +96,7 @@ def test_read_eof(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
# Act # Act
container.seek(100) assert container.seek(100) == 100
data = container.read() data = container.read()
# Assert # Assert
@ -94,21 +107,65 @@ def test_read_eof(bytesmode: bool) -> None:
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_readline(bytesmode: bool) -> None: def test_readline(bytesmode: bool) -> None:
# 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)
# Act
data = container.readline() data = container.readline()
# Assert
if bytesmode: if bytesmode:
data = data.decode() data = data.decode()
assert data == "This is line 1\n" assert data == "This is line 1\n"
data = container.readline(4)
if bytesmode:
data = data.decode()
assert data == "This"
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_readlines(bytesmode: bool) -> None: def test_readlines(bytesmode: bool) -> None:
expected = [
"This is line 1\n",
"This is line 2\n",
"This is line 3\n",
"This is line 4\n",
"This is line 5\n",
"This is line 6\n",
"This is line 7\n",
"This is line 8\n",
]
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
data = container.readlines()
if bytesmode:
data = [line.decode() for line in data]
assert data == expected
assert container.seek(0) == 0
data = container.readlines(2)
if bytesmode:
data = [line.decode() for line in data]
assert data == expected[:2]
@pytest.mark.parametrize("bytesmode", (True, False))
def test_write(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert container.writable() is False
with pytest.raises(NotImplementedError):
container.write(b"" if bytesmode else "")
with pytest.raises(NotImplementedError):
container.writelines([])
with pytest.raises(NotImplementedError):
container.truncate()
@pytest.mark.parametrize("bytesmode", (True, False))
def test_iter(bytesmode: bool) -> None:
# Arrange # Arrange
expected = [ expected = [
"This is line 1\n", "This is line 1\n",
@ -124,9 +181,21 @@ def test_readlines(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 0, 120) container = ContainerIO.ContainerIO(fh, 0, 120)
# Act # Act
data = container.readlines() data = []
for line in container:
data.append(line)
# Assert # Assert
if bytesmode: if bytesmode:
data = [line.decode() for line in data] data = [line.decode() for line in data]
assert data == expected assert data == expected
@pytest.mark.parametrize("bytesmode", (True, False))
def test_file(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert isinstance(container.fileno(), int)
container.flush()
container.close()

View File

@ -329,46 +329,6 @@ def test_read_binary_preview() -> None:
pass pass
def test_readline_psfile(tmp_path: Path) -> None:
# check all the freaking line endings possible from the spec
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
line_endings = ["\r\n", "\n", "\n\r", "\r"]
strings = ["something", "else", "baz", "bif"]
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
assert t.readline().strip("\r\n") == "something", ending
assert t.readline().strip("\r\n") == "else", ending
assert t.readline().strip("\r\n") == "baz", ending
assert t.readline().strip("\r\n") == "bif", ending
def _test_readline_io_psfile(test_string: str, ending: str) -> None:
f = io.BytesIO(test_string.encode("latin-1"))
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(f)
_test_readline(t, ending)
def _test_readline_file_psfile(test_string: str, ending: str) -> None:
f = str(tmp_path / "temp.txt")
with open(f, "wb") as w:
w.write(test_string.encode("latin-1"))
with open(f, "rb") as r:
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(r)
_test_readline(t, ending)
for ending in line_endings:
s = ending.join(strings)
_test_readline_io_psfile(s, ending)
_test_readline_file_psfile(s, ending)
def test_psfile_deprecation() -> None:
with pytest.warns(DeprecationWarning):
EpsImagePlugin.PSFile(None)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
@pytest.mark.parametrize( @pytest.mark.parametrize(
"line_ending", "line_ending",
@ -425,9 +385,10 @@ def test_timeout(test_file: str) -> None:
def test_bounding_box_in_trailer() -> None: 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 (
FILE1 Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image,
) as header_image: Image.open(FILE1) as header_image,
):
assert trailer_image.size == header_image.size assert trailer_image.size == header_image.size

View File

@ -1,9 +1,9 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from collections.abc import Generator
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Generator
import pytest import pytest
@ -53,6 +53,7 @@ def test_closed_file() -> None:
def test_seek_after_close() -> None: def test_seek_after_close() -> None:
im = Image.open("Tests/images/iss634.gif") im = Image.open("Tests/images/iss634.gif")
assert isinstance(im, GifImagePlugin.GifImageFile)
im.load() im.load()
im.close() im.close()
@ -352,7 +353,7 @@ def test_palette_434(tmp_path: Path) -> None:
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: 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, **kwargs) im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out) reloaded = Image.open(out)
return reloaded return reloaded
@ -377,7 +378,8 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
img = img.convert("RGB") img = img.convert("RGB")
tempfile = str(tmp_path / "temp.gif") tempfile = str(tmp_path / "temp.gif")
GifImagePlugin._save_netpbm(img, 0, tempfile) b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded: with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("RGB"), 0) assert_image_similar(img, reloaded.convert("RGB"), 0)
@ -388,7 +390,8 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
img = img.convert("L") img = img.convert("L")
tempfile = str(tmp_path / "temp.gif") tempfile = str(tmp_path / "temp.gif")
GifImagePlugin._save_netpbm(img, 0, tempfile) b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded: with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("L"), 0) assert_image_similar(img, reloaded.convert("L"), 0)
@ -648,7 +651,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
assert rgb_img.getpixel((50, 50)) == circle assert rgb_img.getpixel((50, 50)) == circle
# Check that frame transparency wasn't added unnecessarily # Check that frame transparency wasn't added unnecessarily
assert img._frame_transparency is None assert getattr(img, "_frame_transparency") is None
def test_dispose2_diff(tmp_path: Path) -> None: def test_dispose2_diff(tmp_path: Path) -> None:
@ -1252,10 +1255,11 @@ def test_palette_save_L(tmp_path: Path) -> None:
im = hopper("P") im = hopper("P")
im_l = Image.frombytes("L", im.size, im.tobytes()) im_l = Image.frombytes("L", im.size, im.tobytes())
palette = bytes(im.getpalette()) palette = im.getpalette()
assert palette is not None
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im_l.save(out, palette=palette) im_l.save(out, palette=bytes(palette))
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))

View File

@ -5,7 +5,7 @@ from typing import IO
import pytest import pytest
from PIL import GribStubImagePlugin, Image from PIL import GribStubImagePlugin, Image, ImageFile
from .helper import hopper from .helper import hopper
@ -51,7 +51,7 @@ def test_save(tmp_path: Path) -> None:
def test_handler(tmp_path: Path) -> None: def test_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler(ImageFile.StubHandler):
opened = False opened = False
loaded = False loaded = False
saved = False saved = False
@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) return Image.new("RGB", (1, 1))
def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True self.saved = True
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
GribStubImagePlugin.register_handler(handler) GribStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert handler.opened assert handler.opened
assert not handler.loaded assert not handler.is_loaded()
im.load() im.load()
assert handler.loaded assert handler.is_loaded()
temp_file = str(tmp_path / "temp.grib") temp_file = str(tmp_path / "temp.grib")
im.save(temp_file) im.save(temp_file)

View File

@ -1,11 +1,12 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import IO from typing import IO
import pytest import pytest
from PIL import Hdf5StubImagePlugin, Image from PIL import Hdf5StubImagePlugin, Image, ImageFile
TEST_FILE = "Tests/images/hdf5.h5" TEST_FILE = "Tests/images/hdf5.h5"
@ -41,7 +42,7 @@ def test_load() -> None:
def test_save() -> None: def test_save() -> None:
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
dummy_fp = None dummy_fp = BytesIO()
dummy_filename = "dummy.filename" dummy_filename = "dummy.filename"
# Act / Assert: stub cannot save without an implemented handler # Act / Assert: stub cannot save without an implemented handler
@ -52,7 +53,7 @@ def test_save() -> None:
def test_handler(tmp_path: Path) -> None: def test_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler(ImageFile.StubHandler):
opened = False opened = False
loaded = False loaded = False
saved = False saved = False
@ -65,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close() im.fp.close()
return Image.new("RGB", (1, 1)) return Image.new("RGB", (1, 1))
def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True self.saved = True
@ -72,10 +76,10 @@ def test_handler(tmp_path: Path) -> None:
Hdf5StubImagePlugin.register_handler(handler) Hdf5StubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert handler.opened assert handler.opened
assert not handler.loaded assert not handler.is_loaded()
im.load() im.load()
assert handler.loaded assert handler.is_loaded()
temp_file = str(tmp_path / "temp.h5") temp_file = str(tmp_path / "temp.h5")
im.save(temp_file) im.save(temp_file)

View File

@ -70,7 +70,9 @@ class TestFileJpeg:
def test_sanity(self) -> None: def test_sanity(self) -> None:
# internal version number # internal version number
assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) version = features.version_codec("jpg")
assert version is not None
assert re.search(r"\d+\.\d+$", version)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()
@ -152,7 +154,7 @@ class TestFileJpeg:
assert k > 0.9 assert k > 0.9
def test_rgb(self) -> None: def test_rgb(self) -> None:
def getchannels(im: Image.Image) -> tuple[int, int, int]: def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]:
return tuple(v[0] for v in im.layer) return tuple(v[0] for v in im.layer)
im = hopper() im = hopper()
@ -169,7 +171,7 @@ class TestFileJpeg:
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
) )
def test_dpi(self, test_image_path: str) -> None: def test_dpi(self, test_image_path: str) -> None:
def test(xdpi: int, ydpi: int | None = None): def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None:
with Image.open(test_image_path) as im: with Image.open(test_image_path) as im:
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
return im.info.get("dpi") return im.info.get("dpi")
@ -441,7 +443,9 @@ class TestFileJpeg:
assert_image(im1, im2.mode, im2.size) assert_image(im1, im2.mode, im2.size)
def test_subsampling(self) -> None: def test_subsampling(self) -> None:
def getsampling(im: Image.Image): def getsampling(
im: JpegImagePlugin.JpegImageFile,
) -> tuple[int, int, int, int, int, int]:
layer = im.layer layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
@ -697,7 +701,7 @@ class TestFileJpeg:
def test_save_cjpeg(self, tmp_path: Path) -> None: def test_save_cjpeg(self, tmp_path: Path) -> None:
with Image.open(TEST_FILE) as img: with Image.open(TEST_FILE) as img:
tempfile = str(tmp_path / "temp.jpg") tempfile = str(tmp_path / "temp.jpg")
JpegImagePlugin._save_cjpeg(img, 0, tempfile) JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright # Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17) assert_image_similar_tofile(img, tempfile, 17)
@ -868,7 +872,7 @@ class TestFileJpeg:
def test_multiple_exif(self) -> None: def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im: with Image.open("Tests/images/multiple_exif.jpg") as im:
assert im.info["exif"] == b"Exif\x00\x00firstsecond" assert im.getexif()[270] == "firstsecond"
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@ -915,24 +919,25 @@ class TestFileJpeg:
with Image.open("Tests/images/icc-after-SOF.jpg") as im: with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile" assert im.info["icc_profile"] == b"profile"
def test_jpeg_magic_number(self) -> None: def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
size = 4097 size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer.max_pos = 0 max_pos = 0
orig_read = buffer.read orig_read = buffer.read
def read(n=-1): def read(n: int | None = -1) -> bytes:
nonlocal max_pos
res = orig_read(n) res = orig_read(n)
buffer.max_pos = max(buffer.max_pos, buffer.tell()) max_pos = max(max_pos, buffer.tell())
return res return res
buffer.read = read monkeypatch.setattr(buffer, "read", read)
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
with Image.open(buffer): with Image.open(buffer):
pass pass
# Assert the entire file has not been read # Assert the entire file has not been read
assert 0 < buffer.max_pos < size assert 0 < max_pos < size
def test_getxmp(self) -> None: def test_getxmp(self) -> None:
with Image.open("Tests/images/xmp_test.jpg") as im: with Image.open("Tests/images/xmp_test.jpg") as im:
@ -943,6 +948,7 @@ class TestFileJpeg:
): ):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert "xmp" in im.info
xmp = im.getxmp() xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"] description = xmp["xmpmeta"]["RDF"]["Description"]
@ -1027,8 +1033,10 @@ class TestFileJpeg:
def test_repr_jpeg(self) -> None: def test_repr_jpeg(self) -> None:
im = hopper() im = hopper()
b = im._repr_jpeg_()
assert b is not None
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: with Image.open(BytesIO(b)) as repr_jpeg:
assert repr_jpeg.format == "JPEG" assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17) assert_image_similar(im, repr_jpeg, 17)

View File

@ -48,7 +48,9 @@ def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
def test_sanity() -> None: def test_sanity() -> None:
# Internal version number # Internal version number
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) version = features.version_codec("jpg_2000")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
with Image.open("Tests/images/test-card-lossless.jp2") as im: with Image.open("Tests/images/test-card-lossless.jp2") as im:
px = im.load() px = im.load()
@ -333,9 +335,15 @@ def test_issue_6194() -> None:
assert im.getpixel((5, 5)) == 31 assert im.getpixel((5, 5)) == 31
def test_unknown_j2k_mode() -> None:
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/unknown_mode.j2k"):
pass
def test_unbound_local() -> None: def test_unbound_local() -> None:
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception. # prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError): with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/unbound_variable.jp2"): with Image.open("Tests/images/unbound_variable.jp2"):
pass pass
@ -458,7 +466,7 @@ def test_plt_marker() -> None:
out.seek(length - 2, os.SEEK_CUR) out.seek(length - 2, os.SEEK_CUR)
def test_9bit(): def test_9bit() -> None:
with Image.open("Tests/images/9bit.j2k") as im: with Image.open("Tests/images/9bit.j2k") as im:
assert im.mode == "I;16" assert im.mode == "I;16"
assert im.size == (128, 128) assert im.size == (128, 128)

View File

@ -52,7 +52,9 @@ class LibTiffTestCase:
class TestFileLibTiff(LibTiffTestCase): class TestFileLibTiff(LibTiffTestCase):
def test_version(self) -> None: def test_version(self) -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) version = features.version_codec("libtiff")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+t?$", version)
def test_g4_tiff(self, tmp_path: Path) -> None: def test_g4_tiff(self, tmp_path: Path) -> None:
"""Test the ordinary file path load path""" """Test the ordinary file path load path"""
@ -90,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object""" """Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif" test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
s.write(f.read()) data = f.read()
s.seek(0)
r = io.BufferedReader(s) class NonBytesIO(io.RawIOBase):
def read(self, size: int = -1) -> bytes:
nonlocal data
if size == -1:
size = len(data)
result = data[:size]
data = data[size:]
return result
def readable(self) -> bool:
return True
r = io.BufferedReader(NonBytesIO())
with Image.open(r) as im: with Image.open(r) as im:
assert im.size == (500, 500) assert im.size == (500, 500)
self._assert_noerr(tmp_path, im) self._assert_noerr(tmp_path, im)
@ -666,7 +679,8 @@ class TestFileLibTiff(LibTiffTestCase):
pilim.save(buffer_io, format="tiff", compression=compression) pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0) buffer_io.seek(0)
assert_image_similar_tofile(pilim, buffer_io, 0) with Image.open(buffer_io) as saved_im:
assert_image_similar(pilim, saved_im, 0)
save_bytesio() save_bytesio()
save_bytesio("raw") save_bytesio("raw")
@ -682,13 +696,18 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
def test_exif_ifd(self, tmp_path: Path) -> None: def test_exif_ifd(self) -> None:
outfile = str(tmp_path / "temp.tif") out = io.BytesIO()
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert im.tag_v2[34665] == 125456 assert im.tag_v2[34665] == 125456
im.save(outfile) im.save(out, "TIFF")
with Image.open(outfile) as reloaded: with Image.open(out) as reloaded:
assert 34665 not in reloaded.tag_v2
im.save(out, "TIFF", tiffinfo={34665: 125456})
with Image.open(out) as reloaded:
if Image.core.libtiff_support_custom_tags: if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456 assert reloaded.tag_v2[34665] == 125456
@ -1040,7 +1059,11 @@ class TestFileLibTiff(LibTiffTestCase):
], ],
) )
def test_wrong_bits_per_sample( def test_wrong_bits_per_sample(
self, file_name: str, mode: str, size: tuple[int, int], tile self,
file_name: str,
mode: str,
size: tuple[int, int],
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
) -> None: ) -> None:
with Image.open("Tests/images/" + file_name) as im: with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode assert im.mode == mode
@ -1127,7 +1150,7 @@ class TestFileLibTiff(LibTiffTestCase):
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument: if argument:
arguments["strip_size"] = 2**18 arguments["strip_size"] = 2**18
im.save(out, **arguments) im.save(out, "TIFF", **arguments)
with Image.open(out) as im: with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile) assert isinstance(im, TiffImagePlugin.TiffImageFile)

View File

@ -2,11 +2,11 @@ from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO
from typing import Any, cast from typing import Any
import pytest import pytest
from PIL import Image, MpoImagePlugin from PIL import Image, ImageFile, MpoImagePlugin
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -20,11 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg") pytestmark = skip_unless_feature("jpg")
def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile: def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
out = BytesIO() out = BytesIO()
im.save(out, "MPO", **options) im.save(out, "MPO", **options)
out.seek(0) out.seek(0)
return cast(MpoImagePlugin.MpoImageFile, Image.open(out)) return Image.open(out)
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
@ -226,6 +226,17 @@ def test_eoferror() -> None:
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)
def test_ultra_hdr() -> None:
with Image.open("Tests/images/ultrahdr.jpg") as im:
assert im.format == "JPEG"
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_image_grab(test_file: str) -> None: def test_image_grab(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
@ -270,6 +281,8 @@ def test_save_all() -> None:
im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded) assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100" assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1) im_reloaded.seek(1)

View File

@ -76,6 +76,7 @@ def test_pil184() -> None:
def test_1px_width(tmp_path: Path) -> None: def test_1px_width(tmp_path: Path) -> None:
im = Image.new("L", (1, 256)) im = Image.new("L", (1, 256))
px = im.load() px = im.load()
assert px is not None
for y in range(256): for y in range(256):
px[0, y] = y px[0, y] = y
_roundtrip(tmp_path, im) _roundtrip(tmp_path, im)
@ -84,6 +85,7 @@ def test_1px_width(tmp_path: Path) -> None:
def test_large_count(tmp_path: Path) -> None: def test_large_count(tmp_path: Path) -> None:
im = Image.new("L", (256, 1)) im = Image.new("L", (256, 1))
px = im.load() px = im.load()
assert px is not None
for x in range(256): for x in range(256):
px[x, 0] = x // 67 * 67 px[x, 0] = x // 67 * 67
_roundtrip(tmp_path, im) _roundtrip(tmp_path, im)
@ -101,6 +103,7 @@ def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) ->
def test_break_in_count_overflow(tmp_path: Path) -> None: def test_break_in_count_overflow(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(4): for y in range(4):
for x in range(256): for x in range(256):
px[x, y] = x % 128 px[x, y] = x % 128
@ -110,6 +113,7 @@ def test_break_in_count_overflow(tmp_path: Path) -> None:
def test_break_one_in_loop(tmp_path: Path) -> None: def test_break_one_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(5): for y in range(5):
for x in range(256): for x in range(256):
px[x, y] = x % 128 px[x, y] = x % 128
@ -119,6 +123,7 @@ def test_break_one_in_loop(tmp_path: Path) -> None:
def test_break_many_in_loop(tmp_path: Path) -> None: def test_break_many_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(4): for y in range(4):
for x in range(256): for x in range(256):
px[x, y] = x % 128 px[x, y] = x % 128
@ -130,6 +135,7 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
def test_break_one_at_end(tmp_path: Path) -> None: def test_break_one_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(5): for y in range(5):
for x in range(256): for x in range(256):
px[x, y] = x % 128 px[x, y] = x % 128
@ -140,6 +146,7 @@ def test_break_one_at_end(tmp_path: Path) -> None:
def test_break_many_at_end(tmp_path: Path) -> None: def test_break_many_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5)) im = Image.new("L", (256, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(5): for y in range(5):
for x in range(256): for x in range(256):
px[x, y] = x % 128 px[x, y] = x % 128
@ -152,6 +159,7 @@ def test_break_many_at_end(tmp_path: Path) -> None:
def test_break_padding(tmp_path: Path) -> None: def test_break_padding(tmp_path: Path) -> None:
im = Image.new("L", (257, 5)) im = Image.new("L", (257, 5))
px = im.load() px = im.load()
assert px is not None
for y in range(5): for y in range(5):
for x in range(257): for x in range(257):
px[x, y] = x % 128 px[x, y] = x % 128

View File

@ -5,8 +5,9 @@ import os
import os.path import os.path
import tempfile import tempfile
import time import time
from collections.abc import Generator
from pathlib import Path from pathlib import Path
from typing import Any, Generator from typing import Any
import pytest import pytest
@ -117,7 +118,7 @@ def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper() im = hopper()
outfile = str(tmp_path / "temp.pdf") outfile = str(tmp_path / "temp.pdf")
im.save(outfile, **params) im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp: with open(outfile, "rb") as fp:
contents = fp.read() contents = fp.read()
@ -228,6 +229,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
assert pdf.pages_ref is not None
pages_info = pdf.read_indirect(pdf.pages_ref) pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info assert b"Parent" not in pages_info
assert b"Kids" in pages_info assert b"Kids" in pages_info

View File

@ -41,7 +41,7 @@ MAGIC = PngImagePlugin._MAGIC
def chunk(cid: bytes, *data: bytes) -> bytes: def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO() test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data) PngImagePlugin.putchunk(test_file, cid, *data)
return test_file.getvalue() return test_file.getvalue()
@ -85,9 +85,9 @@ class TestFilePng:
def test_sanity(self, tmp_path: Path) -> None: def test_sanity(self, tmp_path: Path) -> None:
# internal version number # internal version number
assert re.search( version = features.version_codec("zlib")
r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib") assert version is not None
) assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version)
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
@ -535,8 +535,10 @@ class TestFilePng:
def test_repr_png(self) -> None: def test_repr_png(self) -> None:
im = hopper() im = hopper()
b = im._repr_png_()
assert b is not None
with Image.open(BytesIO(im._repr_png_())) as repr_png: with Image.open(BytesIO(b)) as repr_png:
assert repr_png.format == "PNG" assert repr_png.format == "PNG"
assert_image_equal(im, repr_png) assert_image_equal(im, repr_png)
@ -655,11 +657,12 @@ class TestFilePng:
png.call(cid, 0, 0) png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_specify_bits(self, tmp_path: Path) -> None: @pytest.mark.parametrize("save_all", (True, False))
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
im = hopper("P") im = hopper("P")
out = str(tmp_path / "temp.png") out = str(tmp_path / "temp.png")
im.save(out, bits=4) im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 48 assert len(reloaded.png.im_palette[1]) == 48
@ -683,6 +686,7 @@ class TestFilePng:
): ):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert "xmp" in im.info
xmp = im.getxmp() xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"] description = xmp["xmpmeta"]["RDF"]["Description"]
@ -767,16 +771,12 @@ class TestFilePng:
def test_save_stdout(self, buffer: bool) -> None: def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer: class MyStdOut:
buffer = BytesIO()
class MyStdOut: mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
buffer = BytesIO()
mystdout = MyStdOut() sys.stdout = mystdout # type: ignore[assignment]
else:
mystdout = BytesIO()
sys.stdout = mystdout
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG") im.save(sys.stdout, "PNG")
@ -784,7 +784,7 @@ class TestFilePng:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if buffer: if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded: with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE) assert_image_equal_tofile(reloaded, TEST_PNG_FILE)

View File

@ -368,16 +368,12 @@ def test_mimetypes(tmp_path: Path) -> None:
def test_save_stdout(buffer: bool) -> None: def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer: class MyStdOut:
buffer = BytesIO()
class MyStdOut: mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
buffer = BytesIO()
mystdout = MyStdOut() sys.stdout = mystdout # type: ignore[assignment]
else:
mystdout = BytesIO()
sys.stdout = mystdout
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM") im.save(sys.stdout, "PPM")
@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if buffer: if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded: with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_FILE) assert_image_equal_tofile(reloaded, TEST_FILE)

View File

@ -4,7 +4,7 @@ import warnings
import pytest import pytest
from PIL import Image, PsdImagePlugin, UnidentifiedImageError from PIL import Image, PsdImagePlugin
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
@ -150,20 +150,26 @@ def test_combined_larger_than_size() -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file,raises", "test_file,raises",
[ [
(
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
UnidentifiedImageError,
),
(
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
UnidentifiedImageError,
),
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
], ],
) )
def test_crashes(test_file: str, raises) -> None: def test_crashes(test_file: str, raises: type[Exception]) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
with pytest.raises(raises): with pytest.raises(raises):
with Image.open(f): with Image.open(f):
pass pass
@pytest.mark.parametrize(
"test_file",
[
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
],
)
def test_layer_crashes(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(SyntaxError):
im.layers

View File

@ -105,6 +105,7 @@ def test_load_image_series() -> None:
img_list = SpiderImagePlugin.loadImageSeries(file_list) img_list = SpiderImagePlugin.loadImageSeries(file_list)
# Assert # Assert
assert img_list is not None
assert len(img_list) == 1 assert len(img_list) == 1
assert isinstance(img_list[0], Image.Image) assert isinstance(img_list[0], Image.Image)
assert img_list[0].size == (128, 128) assert img_list[0].size == (128, 128)

View File

@ -72,12 +72,21 @@ def test_palette_depth_8(tmp_path: Path) -> None:
def test_palette_depth_16(tmp_path: Path) -> None: def test_palette_depth_16(tmp_path: Path) -> None:
with Image.open("Tests/images/p_16.tga") as im: with Image.open("Tests/images/p_16.tga") as im:
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") assert im.palette.mode == "RGBA"
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
out = str(tmp_path / "temp.png") out = str(tmp_path / "temp.png")
im.save(out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
def test_rgba_16() -> None:
with Image.open("Tests/images/rgba16.tga") as im:
assert im.mode == "RGBA"
assert im.getpixel((0, 0)) == (172, 0, 255, 255)
assert im.getpixel((1, 0)) == (0, 255, 82, 0)
def test_id_field() -> None: def test_id_field() -> None:

View File

@ -2,10 +2,10 @@ from __future__ import annotations
import os import os
import warnings import warnings
from collections.abc import Generator
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Generator
import pytest import pytest
@ -78,6 +78,7 @@ class TestFileTiff:
def test_seek_after_close(self) -> None: def test_seek_after_close(self) -> None:
im = Image.open("Tests/images/multipage.tiff") im = Image.open("Tests/images/multipage.tiff")
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.close() im.close()
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -113,14 +114,14 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_seek_too_large(self): def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"): with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif") Image.open("Tests/images/seek_too_large.tif")
def test_set_legacy_api(self) -> None: def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:
ifd.legacy_api = None ifd.legacy_api = False
assert str(e.value) == "Not allowing setting of legacy api" assert str(e.value) == "Not allowing setting of legacy api"
def test_xyres_tiff(self) -> None: def test_xyres_tiff(self) -> None:
@ -424,13 +425,13 @@ class TestFileTiff:
def test_load_float(self) -> None: def test_load_float(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdabcd" data = b"abcdabcd"
ret = ifd.load_float(data, False) ret = getattr(ifd, "load_float")(data, False)
assert ret == (1.6777999408082104e22, 1.6777999408082104e22) assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
def test_load_double(self) -> None: def test_load_double(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdefghabcdefgh" data = b"abcdefghabcdefgh"
ret = ifd.load_double(data, False) ret = getattr(ifd, "load_double")(data, False)
assert ret == (8.540883223036124e194, 8.540883223036124e194) assert ret == (8.540883223036124e194, 8.540883223036124e194)
def test_ifd_tag_type(self) -> None: def test_ifd_tag_type(self) -> None:
@ -599,7 +600,7 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None: def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
filename = str(tmp_path / "temp.tif") filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename, **kwargs) hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im: with Image.open(filename) as im:
# legacy interface # legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[X_RESOLUTION][0][0] == 72
@ -621,6 +622,22 @@ class TestFileTiff:
assert_image_equal_tofile(im, tmpfile) assert_image_equal_tofile(im, tmpfile)
def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/hopper.tif") as im:
im.load()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[33723] = 1
ifd.tagtype[33723] = 4
im.tag_v2 = ifd
im.save(outfile)
with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 33723 not in im.tag_v2
def test_rowsperstrip(self, tmp_path: Path) -> None: def test_rowsperstrip(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im = hopper() im = hopper()
@ -759,6 +776,7 @@ class TestFileTiff:
): ):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert "xmp" in im.info
xmp = im.getxmp() xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"] description = xmp["xmpmeta"]["RDF"]["Description"]

View File

@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import assert_deep_equal, hopper from .helper import assert_deep_equal, hopper
TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} TAG_IDS: dict[str, int] = {
info.name: info.value
for info in TiffTags.TAGS_V2.values()
if info.value is not None
}
def test_rt_metadata(tmp_path: Path) -> None: def test_rt_metadata(tmp_path: Path) -> None:
@ -411,8 +415,8 @@ def test_empty_values() -> None:
info = TiffImagePlugin.ImageFileDirectory_v2(head) info = TiffImagePlugin.ImageFileDirectory_v2(head)
info.load(data) info.load(data)
# Should not raise ValueError. # Should not raise ValueError.
info = dict(info) info_dict = dict(info)
assert 33432 in info assert 33432 in info_dict
def test_photoshop_info(tmp_path: Path) -> None: def test_photoshop_info(tmp_path: Path) -> None:

View File

@ -5,6 +5,7 @@ import re
import sys import sys
import warnings import warnings
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@ -49,7 +50,9 @@ class TestFileWebp:
def test_version(self) -> None: def test_version(self) -> None:
_webp.WebPDecoderVersion() _webp.WebPDecoderVersion()
_webp.WebPDecoderBuggyAlpha() _webp.WebPDecoderBuggyAlpha()
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp")) version = features.version_module("webp")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
def test_read_rgb(self) -> None: def test_read_rgb(self) -> None:
""" """
@ -68,7 +71,9 @@ class TestFileWebp:
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None: def _roundtrip(
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
) -> None:
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
hopper(mode).save(temp_file, **args) hopper(mode).save(temp_file, **args)
@ -196,7 +201,9 @@ class TestFileWebp:
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
) )
@skip_unless_feature("webp_anim") @skip_unless_feature("webp_anim")
def test_invalid_background(self, background, tmp_path: Path) -> None: def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path
) -> None:
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
im = hopper() im = hopper()
with pytest.raises(OSError): with pytest.raises(OSError):

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -52,8 +53,9 @@ def test_write_animation_L(tmp_path: Path) -> None:
assert_image_similar(im, orig.convert("RGBA"), 32.9) assert_image_similar(im, orig.convert("RGBA"), 32.9)
if is_big_endian(): if is_big_endian():
webp = parse_version(features.version_module("webp")) version = features.version_module("webp")
if webp < parse_version("1.2.2"): assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2") pytest.skip("Fails with libwebp earlier than 1.2.2")
orig.seek(orig.n_frames - 1) orig.seek(orig.n_frames - 1)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
@ -68,7 +70,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
are visually similar to the originals. are visually similar to the originals.
""" """
def check(temp_file) -> None: def check(temp_file: str) -> None:
with Image.open(temp_file) as im: with Image.open(temp_file) as im:
assert im.n_frames == 2 assert im.n_frames == 2
@ -78,8 +80,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
# Compare second frame to original # Compare second frame to original
if is_big_endian(): if is_big_endian():
webp = parse_version(features.version_module("webp")) version = features.version_module("webp")
if webp < parse_version("1.2.2"): assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2") pytest.skip("Fails with libwebp earlier than 1.2.2")
im.seek(1) im.seek(1)
im.load() im.load()
@ -94,7 +97,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
check(temp_file1) check(temp_file1)
# 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
temp_file2 = str(tmp_path / "temp_generator.webp") temp_file2 = str(tmp_path / "temp_generator.webp")

View File

@ -129,6 +129,7 @@ def test_getxmp() -> None:
): ):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert "xmp" in im.info
assert ( assert (
im.getxmp()["xmpmeta"]["xmptk"] im.getxmp()["xmpmeta"]["xmptk"]
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 " == "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import IO
import pytest import pytest
from PIL import Image, WmfImagePlugin from PIL import Image, ImageFile, WmfImagePlugin
from .helper import assert_image_similar_tofile, hopper from .helper import assert_image_similar_tofile, hopper
@ -34,10 +35,13 @@ def test_load() -> None:
def test_register_handler(tmp_path: Path) -> None: def test_register_handler(tmp_path: Path) -> None:
class TestHandler: class TestHandler(ImageFile.StubHandler):
methodCalled = False methodCalled = False
def save(self, im, fp, filename) -> None: def load(self, im: ImageFile.StubImageFile) -> Image.Image:
return Image.new("RGB", (1, 1))
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.methodCalled = True self.methodCalled = True
handler = TestHandler() handler = TestHandler()
@ -70,7 +74,7 @@ def test_load_set_dpi() -> None:
@pytest.mark.parametrize("ext", (".wmf", ".emf")) @pytest.mark.parametrize("ext", (".wmf", ".emf"))
def test_save(ext, tmp_path: Path) -> None: def test_save(ext: str, tmp_path: Path) -> None:
im = hopper() im = hopper()
tmpfile = str(tmp_path / ("temp" + ext)) tmpfile = str(tmp_path / ("temp" + ext))

View File

@ -12,7 +12,7 @@ class TestTTypeFontLeak(PillowLeakTestCase):
iterations = 10 iterations = 10
mem_limit = 4096 # k mem_limit = 4096 # k
def _test_font(self, font: ImageFont.FreeTypeFont) -> None: def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None:
im = Image.new("RGB", (255, 255), "white") im = Image.new("RGB", (255, 255), "white")
draw = ImageDraw.ImageDraw(im) draw = ImageDraw.ImageDraw(im)
self._test_leak( self._test_leak(
@ -34,7 +34,7 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
def test_leak(self) -> None: def test_leak(self) -> None:
if features.check_module("freetype2"): if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError) ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try: try:
default_font = ImageFont.load_default() default_font = ImageFont.load_default()
finally: finally:

View File

@ -8,7 +8,8 @@ import sys
import tempfile import tempfile
import warnings import warnings
from pathlib import Path from pathlib import Path
from typing import IO from types import ModuleType
from typing import IO, Any
import pytest import pytest
@ -25,6 +26,7 @@ from PIL import (
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
assert_image_equal_tofile, assert_image_equal_tofile,
assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
assert_not_all_same, assert_not_all_same,
hopper, hopper,
@ -34,6 +36,12 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
# Deprecation helper # Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -99,10 +107,18 @@ class TestImage:
JPGFILE = "Tests/images/hopper.jpg" JPGFILE = "Tests/images/hopper.jpg"
with pytest.raises(TypeError): with pytest.raises(TypeError):
with Image.open(PNGFILE, formats=123): with Image.open(PNGFILE, formats=123): # type: ignore[arg-type]
pass pass
for formats in [["JPEG"], ("JPEG",), ["jpeg"], ["Jpeg"], ["jPeG"], ["JpEg"]]: format_list: list[list[str] | tuple[str, ...]] = [
["JPEG"],
("JPEG",),
["jpeg"],
["Jpeg"],
["jPeG"],
["JpEg"],
]
for formats in format_list:
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
with Image.open(PNGFILE, formats=formats): with Image.open(PNGFILE, formats=formats):
pass pass
@ -116,6 +132,15 @@ class TestImage:
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (128, 128) assert im.size == (128, 128)
def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True)
im = io.BytesIO(b"")
with pytest.warns(UserWarning):
with pytest.raises(UnidentifiedImageError):
with Image.open(im):
pass
def test_width_height(self) -> None: def test_width_height(self) -> None:
im = Image.new("RGB", (1, 2)) im = Image.new("RGB", (1, 2))
assert im.width == 1 assert im.width == 1
@ -138,12 +163,12 @@ class TestImage:
def test_bad_mode(self) -> None: def test_bad_mode(self) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
with Image.open("filename", "bad mode"): with Image.open("filename", "bad mode"): # type: ignore[arg-type]
pass pass
def test_stringio(self) -> None: def test_stringio(self) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
with Image.open(io.StringIO()): with Image.open(io.StringIO()): # type: ignore[arg-type]
pass pass
def test_pathlib(self, tmp_path: Path) -> None: def test_pathlib(self, tmp_path: Path) -> None:
@ -166,11 +191,19 @@ class TestImage:
def test_fp_name(self, tmp_path: Path) -> None: def test_fp_name(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.jpg") temp_file = str(tmp_path / "temp.jpg")
class FP: class FP(io.BytesIO):
name: str name: str
def write(self, b: bytes) -> None: if sys.version_info >= (3, 12):
pass from collections.abc import Buffer
def write(self, data: Buffer) -> int:
return len(data)
else:
def write(self, data: Any) -> int:
return len(data)
fp = FP() fp = FP()
fp.name = temp_file fp.name = temp_file
@ -185,7 +218,8 @@ class TestImage:
with tempfile.TemporaryFile() as fp: with tempfile.TemporaryFile() as fp:
im.save(fp, "JPEG") im.save(fp, "JPEG")
fp.seek(0) fp.seek(0)
assert_image_similar_tofile(im, fp, 20) with Image.open(fp) as reloaded:
assert_image_similar(im, reloaded, 20)
def test_unknown_extension(self, tmp_path: Path) -> None: def test_unknown_extension(self, tmp_path: Path) -> None:
im = hopper() im = hopper()
@ -338,8 +372,9 @@ class TestImage:
img = Image.alpha_composite(dst, src) img = Image.alpha_composite(dst, src)
# Assert # Assert
img_colors = sorted(img.getcolors()) img_colors = img.getcolors()
assert img_colors == expected_colors assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_inplace(self) -> None: def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue") src = Image.new("RGBA", (128, 128), "blue")
@ -383,13 +418,13 @@ class TestImage:
# errors # errors
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, "invalid source") source.alpha_composite(over, "invalid destination") # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), "invalid destination") source.alpha_composite(over, (0, 0), "invalid source") # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, 0) source.alpha_composite(over, 0) # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), 0) source.alpha_composite(over, (0, 0), 0) # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), (0, -1)) source.alpha_composite(over, (0, 0), (0, -1))
@ -497,9 +532,11 @@ class TestImage:
def test_check_size(self) -> None: def test_check_size(self) -> None:
# Checking that the _check_size function throws value errors when we want it to # Checking that the _check_size function throws value errors when we want it to
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.new("RGB", 0) # not a tuple # not a tuple
Image.new("RGB", 0) # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.new("RGB", (0,)) # Tuple too short # tuple too short
Image.new("RGB", (0,)) # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.new("RGB", (-1, -1)) # w,h < 0 Image.new("RGB", (-1, -1)) # w,h < 0
@ -539,6 +576,7 @@ class TestImage:
for mode in ("I", "F", "L"): for mode in ("I", "F", "L"):
im = Image.new(mode, (100, 100), (5,)) im = Image.new(mode, (100, 100), (5,))
px = im.load() px = im.load()
assert px is not None
assert px[0, 0] == 5 assert px[0, 0] == 5
def test_linear_gradient_wrong_mode(self) -> None: def test_linear_gradient_wrong_mode(self) -> None:
@ -633,7 +671,9 @@ class TestImage:
im_remapped = im.remap_palette([1, 0]) im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1 assert im_remapped.info["transparency"] == 1
assert len(im_remapped.getpalette()) == 6 palette = im_remapped.getpalette()
assert palette is not None
assert len(palette) == 6
# Test unused transparency # Test unused transparency
im.info["transparency"] = 2 im.info["transparency"] = 2
@ -664,7 +704,7 @@ class TestImage:
else: else:
assert new_image.palette is None assert new_image.palette is None
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) _make_new(im, im_p, ImagePalette.ImagePalette("RGB"))
_make_new(im_p, im, None) _make_new(im_p, im, None)
_make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette())
@ -897,6 +937,25 @@ class TestImage:
assert tag not in exif.get_ifd(0x8769) assert tag not in exif.get_ifd(0x8769)
assert exif.get_ifd(0xA005) assert exif.get_ifd(0xA005)
def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im:
assert im.getxmp() == {}
def test_getxmp_padded(self) -> None:
im = Image.new("RGB", (1, 1))
im.info["xmp"] = (
b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00'
)
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {}
else:
assert im.getxmp() == {"xmpmeta": None}
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None: def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size) im = Image.new("RGB", size)

View File

@ -12,19 +12,6 @@ from PIL import Image
from .helper import assert_image_equal, hopper, is_win32 from .helper import assert_image_equal, hopper, is_win32
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
cffi: ModuleType | None
if os.environ.get("PYTHONOPTIMIZE") == "2":
cffi = None
else:
try:
import cffi
from PIL import PyAccess
except ImportError:
cffi = None
numpy: ModuleType | None numpy: ModuleType | None
try: try:
import numpy import numpy
@ -32,21 +19,7 @@ except ImportError:
numpy = None numpy = None
class AccessTest: class TestImagePutPixel:
# Initial value
_init_cffi_access = Image.USE_CFFI_ACCESS
_need_cffi_access = False
@classmethod
def setup_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._need_cffi_access
@classmethod
def teardown_class(cls) -> None:
Image.USE_CFFI_ACCESS = cls._init_cffi_access
class TestImagePutPixel(AccessTest):
def test_sanity(self) -> None: def test_sanity(self) -> None:
im1 = hopper() im1 = hopper()
im2 = Image.new(im1.mode, im1.size, 0) im2 = Image.new(im1.mode, im1.size, 0)
@ -54,7 +27,9 @@ class TestImagePutPixel(AccessTest):
for y in range(im1.size[1]): for y in range(im1.size[1]):
for x in range(im1.size[0]): for x in range(im1.size[0]):
pos = x, y pos = x, y
im2.putpixel(pos, im1.getpixel(pos)) value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
@ -64,7 +39,9 @@ class TestImagePutPixel(AccessTest):
for y in range(im1.size[1]): for y in range(im1.size[1]):
for x in range(im1.size[0]): for x in range(im1.size[0]):
pos = x, y pos = x, y
im2.putpixel(pos, im1.getpixel(pos)) value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly assert not im2.readonly
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
@ -74,10 +51,12 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load() pix1 = im1.load()
pix2 = im2.load() pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
with pytest.raises(TypeError): with pytest.raises(TypeError):
pix1[0, "0"] pix1[0, "0"] # type: ignore[index]
with pytest.raises(TypeError): with pytest.raises(TypeError):
pix1["0", 0] pix1["0", 0] # type: ignore[index]
for y in range(im1.size[1]): for y in range(im1.size[1]):
for x in range(im1.size[0]): for x in range(im1.size[0]):
@ -96,7 +75,9 @@ class TestImagePutPixel(AccessTest):
for y in range(-1, -im1.size[1] - 1, -1): for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1): for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y pos = x, y
im2.putpixel(pos, im1.getpixel(pos)) value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
@ -106,7 +87,9 @@ class TestImagePutPixel(AccessTest):
for y in range(-1, -im1.size[1] - 1, -1): for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1): for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y pos = x, y
im2.putpixel(pos, im1.getpixel(pos)) value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly assert not im2.readonly
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
@ -116,6 +99,8 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load() pix1 = im1.load()
pix2 = im2.load() pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
for y in range(-1, -im1.size[1] - 1, -1): for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1): for x in range(-1, -im1.size[0] - 1, -1):
pix2[x, y] = pix1[x, y] pix2[x, y] = pix1[x, y]
@ -125,13 +110,14 @@ class TestImagePutPixel(AccessTest):
@pytest.mark.skipif(numpy is None, reason="NumPy not installed") @pytest.mark.skipif(numpy is None, reason="NumPy not installed")
def test_numpy(self) -> None: def test_numpy(self) -> None:
im = hopper() im = hopper()
pix = im.load() px = im.load()
assert px is not None
assert numpy is not None assert numpy is not None
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) assert px[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
class TestImageGetPixel(AccessTest): class TestImageGetPixel:
@staticmethod @staticmethod
def color(mode: str) -> int | tuple[int, ...]: def color(mode: str) -> int | tuple[int, ...]:
bands = Image.getmodebands(mode) bands = Image.getmodebands(mode)
@ -144,9 +130,6 @@ class TestImageGetPixel(AccessTest):
return tuple(range(1, bands + 1)) return tuple(range(1, bands + 1))
def check(self, mode: str, expected_color_int: int | None = None) -> None: def check(self, mode: str, expected_color_int: int | None = None) -> None:
if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes")
expected_color = ( expected_color = (
self.color(mode) if expected_color_int is None else expected_color_int self.color(mode) if expected_color_int is None else expected_color_int
) )
@ -171,15 +154,14 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with None initial color # Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None) im = Image.new(mode, (0, 0), None)
assert im.load() is not None assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError with pytest.raises(IndexError):
with pytest.raises(error):
im.putpixel((0, 0), expected_color) im.putpixel((0, 0), expected_color)
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check negative index # Check negative index
with pytest.raises(error): with pytest.raises(IndexError):
im.putpixel((-1, -1), expected_color) im.putpixel((-1, -1), expected_color)
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
# Check initial color # Check initial color
@ -199,10 +181,10 @@ class TestImageGetPixel(AccessTest):
# Check 0x0 image with initial color # Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color) im = Image.new(mode, (0, 0), expected_color)
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check negative index # Check negative index
with pytest.raises(error): with pytest.raises(IndexError):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
@pytest.mark.parametrize("mode", Image.MODES) @pytest.mark.parametrize("mode", Image.MODES)
@ -235,120 +217,7 @@ class TestImageGetPixel(AccessTest):
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
@pytest.mark.filterwarnings("ignore::DeprecationWarning") class TestImagePutPixelError:
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffiPutPixel(TestImagePutPixel):
_need_cffi_access = True
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffiGetPixel(TestImageGetPixel):
_need_cffi_access = True
@pytest.mark.skipif(cffi is None, reason="No CFFI")
class TestCffi(AccessTest):
_need_cffi_access = True
def _test_get_access(self, im: Image.Image) -> None:
"""Do we get the same thing as the old pixel access
Using private interfaces, forcing a capi access and
a pyaccess for the same image"""
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
w, h = im.size
for x in range(0, w, 10):
for y in range(0, h, 10):
assert access[(x, y)] == caccess[(x, y)]
# Access an out-of-range pixel
with pytest.raises(ValueError):
access[(access.xsize + 1, access.ysize + 1)]
def test_get_vs_c(self) -> None:
with pytest.warns(DeprecationWarning):
rgb = hopper("RGB")
rgb.load()
self._test_get_access(rgb)
for mode in ("RGBA", "L", "LA", "1", "P", "F"):
self._test_get_access(hopper(mode))
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
self._test_get_access(im)
def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image?
Using private interfaces, forcing a capi access and
a pyaccess for the same image"""
caccess = im.im.pixel_access(False)
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
w, h = im.size
for x in range(0, w, 10):
for y in range(0, h, 10):
access[(x, y)] = color
assert color == caccess[(x, y)]
# Attempt to set the value on a read-only image
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, True)
with pytest.raises(ValueError):
access[(0, 0)] = color
def test_set_vs_c(self) -> None:
rgb = hopper("RGB")
with pytest.warns(DeprecationWarning):
rgb.load()
self._test_set_access(rgb, (255, 128, 0))
self._test_set_access(hopper("RGBA"), (255, 192, 128, 0))
self._test_set_access(hopper("L"), 128)
self._test_set_access(hopper("LA"), (128, 128))
self._test_set_access(hopper("1"), 255)
self._test_set_access(hopper("P"), 128)
self._test_set_access(hopper("PA"), (128, 128))
self._test_set_access(hopper("F"), 1024.0)
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
self._test_set_access(im, 45000)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_not_implemented(self) -> None:
assert PyAccess.new(hopper("BGR;15")) is None
# Ref https://github.com/python-pillow/Pillow/pull/2009
def test_reference_counting(self) -> None:
size = 10
for _ in range(10):
# Do not save references to the image, only to the access object
with pytest.warns(DeprecationWarning):
px = Image.new("L", (size, 1), 0).load()
for i in range(size):
# Pixels can contain garbage if image is released
assert px[i, 0] == 0
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_p_putpixel_rgb_rgba(self, mode: str) -> None:
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1))
with pytest.warns(DeprecationWarning):
access = PyAccess.new(im, False)
access.putpixel((0, 0), color)
if len(color) == 3:
color += (255,)
assert im.convert("RGBA").getpixel((0, 0)) == color
class TestImagePutPixelError(AccessTest):
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"]
IMAGE_MODES2 = ["L", "I", "I;16"] IMAGE_MODES2 = ["L", "I", "I;16"]
INVALID_TYPES = ["foo", 1.0, None] INVALID_TYPES = ["foo", 1.0, None]
@ -358,7 +227,7 @@ class TestImagePutPixelError(AccessTest):
im = hopper(mode) im = hopper(mode)
for v in self.INVALID_TYPES: for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"): with pytest.raises(TypeError, match="color must be int or tuple"):
im.putpixel((0, 0), v) im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mode", "band_numbers", "match"), ("mode", "band_numbers", "match"),
@ -392,7 +261,7 @@ class TestImagePutPixelError(AccessTest):
with pytest.raises( with pytest.raises(
TypeError, match="color must be int or single-element tuple" TypeError, match="color must be int or single-element tuple"
): ):
im.putpixel((0, 0), v) im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
def test_putpixel_overflow_error(self, mode: str) -> None: def test_putpixel_overflow_error(self, mode: str) -> None:

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -13,13 +13,16 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
im = hopper().resize((128, 100)) im = hopper().resize((128, 100))
if TYPE_CHECKING:
import numpy.typing as npt
def test_toarray() -> None: def test_toarray() -> None:
def test(mode: str) -> tuple[tuple[int, ...], str, int]: def test(mode: str) -> tuple[tuple[int, ...], str, int]:
ai = numpy.array(im.convert(mode)) ai = numpy.array(im.convert(mode))
return ai.shape, ai.dtype.str, ai.nbytes return ai.shape, ai.dtype.str, ai.nbytes
def test_with_dtype(dtype) -> None: def test_with_dtype(dtype: npt.DTypeLike) -> None:
ai = numpy.array(im, dtype=dtype) ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype assert ai.dtype == dtype
@ -86,8 +89,8 @@ def test_fromarray() -> None:
assert test("RGBX") == ("RGBA", (128, 100), True) assert test("RGBX") == ("RGBA", (128, 100), True)
# Test mode is None with no "typestr" in the array interface # Test mode is None with no "typestr" in the array interface
wrapped = Wrapper(hopper("L"), {"shape": (100, 128)})
with pytest.raises(TypeError): with pytest.raises(TypeError):
wrapped = Wrapper(test("L"), {"shape": (100, 128)})
Image.fromarray(wrapped) Image.fromarray(wrapped)

View File

@ -222,8 +222,10 @@ def test_l_macro_rounding(convert_mode: str) -> None:
converted_im = im.convert(convert_mode) converted_im = im.convert(convert_mode)
px = converted_im.load() px = converted_im.load()
assert px is not None
converted_color = px[0, 0] converted_color = px[0, 0]
if convert_mode == "LA": if convert_mode == "LA":
assert isinstance(converted_color, tuple)
converted_color = converted_color[0] converted_color = converted_color[0]
assert converted_color == 1 assert converted_color == 1

View File

@ -18,7 +18,7 @@ def test_crop(mode: str) -> None:
def test_wide_crop() -> None: def test_wide_crop() -> None:
def crop(*bbox: int) -> tuple[int, ...]: def crop(bbox: tuple[int, int, int, int]) -> tuple[int, ...]:
i = im.crop(bbox) i = im.crop(bbox)
h = i.histogram() h = i.histogram()
while h and not h[-1]: while h and not h[-1]:
@ -27,23 +27,23 @@ def test_wide_crop() -> None:
im = Image.new("L", (100, 100), 1) im = Image.new("L", (100, 100), 1)
assert crop(0, 0, 100, 100) == (0, 10000) assert crop((0, 0, 100, 100)) == (0, 10000)
assert crop(25, 25, 75, 75) == (0, 2500) assert crop((25, 25, 75, 75)) == (0, 2500)
# sides # sides
assert crop(-25, 0, 25, 50) == (1250, 1250) assert crop((-25, 0, 25, 50)) == (1250, 1250)
assert crop(0, -25, 50, 25) == (1250, 1250) assert crop((0, -25, 50, 25)) == (1250, 1250)
assert crop(75, 0, 125, 50) == (1250, 1250) assert crop((75, 0, 125, 50)) == (1250, 1250)
assert crop(0, 75, 50, 125) == (1250, 1250) assert crop((0, 75, 50, 125)) == (1250, 1250)
assert crop(-25, 25, 125, 75) == (2500, 5000) assert crop((-25, 25, 125, 75)) == (2500, 5000)
assert crop(25, -25, 75, 125) == (2500, 5000) assert crop((25, -25, 75, 125)) == (2500, 5000)
# corners # corners
assert crop(-25, -25, 25, 25) == (1875, 625) assert crop((-25, -25, 25, 25)) == (1875, 625)
assert crop(75, -25, 125, 25) == (1875, 625) assert crop((75, -25, 125, 25)) == (1875, 625)
assert crop(75, 75, 125, 125) == (1875, 625) assert crop((75, 75, 125, 125)) == (1875, 625)
assert crop(-25, 75, 25, 125) == (1875, 625) assert crop((-25, 75, 25, 125)) == (1875, 625)
@pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) @pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2)))

View File

@ -16,7 +16,9 @@ def draft_roundtrip(
im = Image.new(in_mode, in_size) im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG") data = tostring(im, "JPEG")
im = fromstring(data) im = fromstring(data)
mode, box = im.draft(req_mode, req_size) result = im.draft(req_mode, req_size)
assert result is not None
box = result[1]
scale, _ = im.decoderconfig scale, _ = im.decoderconfig
assert box[:2] == (0, 0) assert box[:2] == (0, 0)
assert (im.width - scale) < box[2] <= im.width assert (im.width - scale) < box[2] <= im.width

View File

@ -46,9 +46,9 @@ def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None:
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity_error(mode: str) -> None: def test_sanity_error(mode: str) -> None:
im = hopper(mode)
with pytest.raises(TypeError): with pytest.raises(TypeError):
im = hopper(mode) im.filter("hello") # type: ignore[arg-type]
im.filter("hello")
# crashes on small images # crashes on small images
@ -137,7 +137,7 @@ def test_builtinfilter_p() -> None:
builtin_filter = ImageFilter.BuiltinFilter() builtin_filter = ImageFilter.BuiltinFilter()
with pytest.raises(ValueError): with pytest.raises(ValueError):
builtin_filter.filter(hopper("P")) builtin_filter.filter(hopper("P").im)
def test_kernel_not_enough_coefficients() -> None: def test_kernel_not_enough_coefficients() -> None:

View File

@ -54,17 +54,21 @@ def test_pack() -> None:
assert A is None assert A is None
A = im.getcolors(maxcolors=3) A = im.getcolors(maxcolors=3)
assert A is not None
A.sort() A.sort()
assert A == expected assert A == expected
A = im.getcolors(maxcolors=4) A = im.getcolors(maxcolors=4)
assert A is not None
A.sort() A.sort()
assert A == expected assert A == expected
A = im.getcolors(maxcolors=8) A = im.getcolors(maxcolors=8)
assert A is not None
A.sort() A.sort()
assert A == expected assert A == expected
A = im.getcolors(maxcolors=16) A = im.getcolors(maxcolors=16)
assert A is not None
A.sort() A.sort()
assert A == expected assert A == expected

View File

@ -6,7 +6,7 @@ from .helper import hopper
def test_extrema() -> None: def test_extrema() -> None:
def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]: def extrema(mode: str) -> tuple[float, float] | tuple[tuple[int, int], ...]:
return hopper(mode).getextrema() return hopper(mode).getextrema()
assert extrema("1") == (0, 255) assert extrema("1") == (0, 255)

View File

@ -12,9 +12,10 @@ from .helper import hopper
def test_sanity() -> None: def test_sanity() -> None:
im = hopper() im = hopper()
pix = im.load() px = im.load()
assert pix[0, 0] == (20, 20, 70) assert px is not None
assert px[0, 0] == (20, 20, 70)
def test_close() -> None: def test_close() -> None:

View File

@ -68,7 +68,11 @@ def test_sanity() -> None:
), ),
) )
def test_properties( def test_properties(
mode, expected_base, expected_type, expected_bands, expected_band_names mode: str,
expected_base: str,
expected_type: str,
expected_bands: int,
expected_band_names: tuple[str, ...],
) -> None: ) -> None:
assert Image.getmodebase(mode) == expected_base assert Image.getmodebase(mode) == expected_base
assert Image.getmodetype(mode) == expected_type assert Image.getmodetype(mode) == expected_type

View File

@ -14,6 +14,7 @@ class TestImagingPaste:
self, im: Image.Image, expected: list[tuple[int, int, int, int]] self, im: Image.Image, expected: list[tuple[int, int, int, int]]
) -> None: ) -> None:
px = im.load() px = im.load()
assert px is not None
actual = [ actual = [
px[0, 0], px[0, 0],
px[self.size // 2, 0], px[self.size // 2, 0],
@ -48,6 +49,7 @@ class TestImagingPaste:
def mask_1(self) -> Image.Image: def mask_1(self) -> Image.Image:
mask = Image.new("1", (self.size, self.size)) mask = Image.new("1", (self.size, self.size))
px = mask.load() px = mask.load()
assert px is not None
for y in range(mask.height): for y in range(mask.height):
for x in range(mask.width): for x in range(mask.width):
px[y, x] = (x + y) % 2 px[y, x] = (x + y) % 2
@ -61,6 +63,7 @@ class TestImagingPaste:
def gradient_L(self) -> Image.Image: def gradient_L(self) -> Image.Image:
gradient = Image.new("L", (self.size, self.size)) gradient = Image.new("L", (self.size, self.size))
px = gradient.load() px = gradient.load()
assert px is not None
for y in range(gradient.height): for y in range(gradient.height):
for x in range(gradient.width): for x in range(gradient.width):
px[y, x] = (x + y) % 255 px[y, x] = (x + y) % 255
@ -338,3 +341,8 @@ class TestImagingPaste:
im.copy().paste(im2) im.copy().paste(im2)
im.copy().paste(im2, (0, 0)) im.copy().paste(im2, (0, 0))
def test_incorrect_abbreviated_form(self) -> None:
im = Image.new("L", (1, 1))
with pytest.raises(ValueError):
im.paste(im, im, im)

View File

@ -61,4 +61,4 @@ def test_f_lut() -> None:
def test_f_mode() -> None: def test_f_mode() -> None:
im = hopper("F") im = hopper("F")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.point(None) im.point([])

View File

@ -31,7 +31,7 @@ def test_sanity() -> None:
def test_long_integers() -> None: def test_long_integers() -> None:
# see bug-200802-systemerror # see bug-200802-systemerror
def put(value: int) -> tuple[int, int, int, int]: def put(value: int) -> float | tuple[int, ...] | None:
im = Image.new("RGBA", (1, 1)) im = Image.new("RGBA", (1, 1))
im.putdata([value]) im.putdata([value])
return im.getpixel((0, 0)) return im.getpixel((0, 0))

View File

@ -79,6 +79,7 @@ def test_putpalette_with_alpha_values() -> None:
( (
("RGBA", (1, 2, 3, 4)), ("RGBA", (1, 2, 3, 4)),
("RGBAX", (1, 2, 3, 4, 0)), ("RGBAX", (1, 2, 3, 4, 0)),
("ARGB", (4, 1, 2, 3)),
), ),
) )
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:

View File

@ -24,13 +24,16 @@ def test_sanity() -> None:
def test_libimagequant_quantize() -> None: def test_libimagequant_quantize() -> None:
image = hopper() image = hopper()
if is_ppc64le(): if is_ppc64le():
libimagequant = parse_version(features.version_feature("libimagequant")) version = features.version_feature("libimagequant")
if libimagequant < parse_version("4"): assert version is not None
if parse_version(version) < parse_version("4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P" assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15) assert_image_similar(converted.convert("RGB"), image, 15)
assert len(converted.getcolors()) == 100 colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_octree_quantize() -> None: def test_octree_quantize() -> None:
@ -38,7 +41,9 @@ def test_octree_quantize() -> None:
converted = image.quantize(100, Image.Quantize.FASTOCTREE) converted = image.quantize(100, Image.Quantize.FASTOCTREE)
assert converted.mode == "P" assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 20) assert_image_similar(converted.convert("RGB"), image, 20)
assert len(converted.getcolors()) == 100 colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_rgba_quantize() -> None: def test_rgba_quantize() -> None:
@ -79,6 +84,7 @@ def test_quantize_no_dither2() -> None:
assert tuple(quantized.palette.palette) == data assert tuple(quantized.palette.palette) == data
px = quantized.load() px = quantized.load()
assert px is not None
for x in range(9): for x in range(9):
assert px[x, 0] == (0 if x < 5 else 1) assert px[x, 0] == (0 if x < 5 else 1)
@ -97,7 +103,7 @@ def test_quantize_dither_diff() -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE) "method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
) )
def test_quantize_kmeans(method) -> None: def test_quantize_kmeans(method: Image.Quantize) -> None:
im = hopper() im = hopper()
no_kmeans = im.quantize(kmeans=0, method=method) no_kmeans = im.quantize(kmeans=0, method=method)
kmeans = im.quantize(kmeans=1, method=method) kmeans = im.quantize(kmeans=1, method=method)
@ -117,10 +123,12 @@ def test_colors() -> None:
def test_transparent_colors_equal() -> None: def test_transparent_colors_equal() -> None:
im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) im = Image.new("RGBA", (1, 2), (0, 0, 0, 0))
px = im.load() px = im.load()
assert px is not None
px[0, 1] = (255, 255, 255, 0) px[0, 1] = (255, 255, 255, 0)
converted = im.quantize() converted = im.quantize()
converted_px = converted.load() converted_px = converted.load()
assert converted_px is not None
assert converted_px[0, 0] == converted_px[0, 1] assert converted_px[0, 0] == converted_px[0, 1]
@ -138,6 +146,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
converted = im.quantize(method=method) converted = im.quantize(method=method)
converted_px = converted.load() converted_px = converted.load()
assert converted_px is not None
assert converted_px[0, 0] == converted.palette.colors[color] assert converted_px[0, 0] == converted.palette.colors[color]
@ -153,4 +162,6 @@ def test_small_palette() -> None:
im = im.quantize(palette=p) im = im.quantize(palette=p)
# Assert # Assert
assert len(im.getcolors()) == 2 quantized_colors = im.getcolors()
assert quantized_colors is not None
assert len(quantized_colors) == 2

View File

@ -56,10 +56,12 @@ def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) ->
@pytest.mark.parametrize( @pytest.mark.parametrize(
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
) )
def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None: def test_args_factor_error(
size: float | tuple[int, int], expected_error: type[Exception]
) -> None:
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
with pytest.raises(expected_error): with pytest.raises(expected_error):
im.reduce(size) im.reduce(size) # type: ignore[arg-type]
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -86,10 +88,12 @@ def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) ->
((5, 0, 5, 10), ValueError), ((5, 0, 5, 10), ValueError),
), ),
) )
def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None: def test_args_box_error(
size: str | tuple[int, int, int, int], expected_error: type[Exception]
) -> None:
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
with pytest.raises(expected_error): with pytest.raises(expected_error):
im.reduce(2, size).size im.reduce(2, size).size # type: ignore[arg-type]
@pytest.mark.parametrize("mode", ("P", "1", "I;16")) @pytest.mark.parametrize("mode", ("P", "1", "I;16"))
@ -102,7 +106,7 @@ def test_unsupported_modes(mode: str) -> None:
def get_image(mode: str) -> Image.Image: def get_image(mode: str) -> Image.Image:
mode_info = ImageMode.getmode(mode) mode_info = ImageMode.getmode(mode)
if mode_info.basetype == "L": if mode_info.basetype == "L":
bands = [gradients_image] bands: list[Image.Image] = [gradients_image]
for _ in mode_info.bands[1:]: for _ in mode_info.bands[1:]:
# rotate previous image # rotate previous image
band = bands[-1].transpose(Image.Transpose.ROTATE_90) band = bands[-1].transpose(Image.Transpose.ROTATE_90)

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
from contextlib import contextmanager from contextlib import contextmanager
from typing import Generator
import pytest import pytest
@ -74,6 +74,7 @@ class TestImagingCoreResampleAccuracy:
data = data.replace(" ", "") data = data.replace(" ", "")
sample = Image.new("L", size) sample = Image.new("L", size)
s_px = sample.load() s_px = sample.load()
assert s_px is not None
w, h = size[0] // 2, size[1] // 2 w, h = size[0] // 2, size[1] // 2
for y in range(h): for y in range(h):
for x in range(w): for x in range(w):
@ -87,6 +88,8 @@ class TestImagingCoreResampleAccuracy:
def check_case(self, case: Image.Image, sample: Image.Image) -> None: def check_case(self, case: Image.Image, sample: Image.Image) -> None:
s_px = sample.load() s_px = sample.load()
c_px = case.load() c_px = case.load()
assert s_px is not None
assert c_px is not None
for y in range(case.size[1]): for y in range(case.size[1]):
for x in range(case.size[0]): for x in range(case.size[0]):
if c_px[x, y] != s_px[x, y]: if c_px[x, y] != s_px[x, y]:
@ -98,6 +101,7 @@ class TestImagingCoreResampleAccuracy:
def serialize_image(self, image: Image.Image) -> str: def serialize_image(self, image: Image.Image) -> str:
s_px = image.load() s_px = image.load()
assert s_px is not None
return "\n".join( return "\n".join(
" ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0]))
for y in range(image.size[1]) for y in range(image.size[1])
@ -233,13 +237,16 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency: class TestCoreResampleConsistency:
def make_case( def make_case(
self, mode: str, fill: tuple[int, int, int] | float self, mode: str, fill: tuple[int, int, int] | float
) -> tuple[Image.Image, tuple[int, ...]]: ) -> tuple[Image.Image, float | tuple[int, ...]]:
im = Image.new(mode, (512, 9), fill) im = Image.new(mode, (512, 9), fill)
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] px = im.load()
assert px is not None
return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0]
def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None: def run_case(self, case: tuple[Image.Image, float | tuple[int, ...]]) -> None:
channel, color = case channel, color = case
px = channel.load() px = channel.load()
assert px is not None
for x in range(channel.size[0]): for x in range(channel.size[0]):
for y in range(channel.size[1]): for y in range(channel.size[1]):
if px[x, y] != color: if px[x, y] != color:
@ -249,6 +256,7 @@ class TestCoreResampleConsistency:
def test_8u(self) -> None: def test_8u(self) -> None:
im, color = self.make_case("RGB", (0, 64, 255)) im, color = self.make_case("RGB", (0, 64, 255))
r, g, b = im.split() r, g, b = im.split()
assert isinstance(color, tuple)
self.run_case((r, color[0])) self.run_case((r, color[0]))
self.run_case((g, color[1])) self.run_case((g, color[1]))
self.run_case((b, color[2])) self.run_case((b, color[2]))
@ -271,6 +279,7 @@ class TestCoreResampleAlphaCorrect:
def make_levels_case(self, mode: str) -> Image.Image: def make_levels_case(self, mode: str) -> Image.Image:
i = Image.new(mode, (256, 16)) i = Image.new(mode, (256, 16))
px = i.load() px = i.load()
assert px is not None
for y in range(i.size[1]): for y in range(i.size[1]):
for x in range(i.size[0]): for x in range(i.size[0]):
pix = [x] * len(mode) pix = [x] * len(mode)
@ -280,8 +289,13 @@ class TestCoreResampleAlphaCorrect:
def run_levels_case(self, i: Image.Image) -> None: def run_levels_case(self, i: Image.Image) -> None:
px = i.load() px = i.load()
assert px is not None
for y in range(i.size[1]): for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])} used_colors = set()
for x in range(i.size[0]):
value = px[x, y]
assert isinstance(value, tuple)
used_colors.add(value[0])
assert 256 == len(used_colors), ( assert 256 == len(used_colors), (
"All colors should be present in resized image. " "All colors should be present in resized image. "
f"Only {len(used_colors)} on line {y}." f"Only {len(used_colors)} on line {y}."
@ -310,6 +324,7 @@ class TestCoreResampleAlphaCorrect:
) -> Image.Image: ) -> Image.Image:
i = Image.new(mode, (64, 64), dirty_pixel) i = Image.new(mode, (64, 64), dirty_pixel)
px = i.load() px = i.load()
assert px is not None
xdiv4 = i.size[0] // 4 xdiv4 = i.size[0] // 4
ydiv4 = i.size[1] // 4 ydiv4 = i.size[1] // 4
for y in range(ydiv4 * 2): for y in range(ydiv4 * 2):
@ -319,14 +334,16 @@ class TestCoreResampleAlphaCorrect:
def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None:
px = i.load() px = i.load()
assert px is not None
for y in range(i.size[1]): for y in range(i.size[1]):
for x in range(i.size[0]): for x in range(i.size[0]):
if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: value = px[x, y]
assert isinstance(value, tuple)
if value[-1] != 0 and value[:-1] != clean_pixel:
message = ( message = (
f"pixel at ({x}, {y}) is different:\n" f"pixel at ({x}, {y}) is different:\n{value}\n{clean_pixel}"
f"{px[x, y]}\n{clean_pixel}"
) )
assert px[x, y][:3] == clean_pixel, message assert value[:3] == clean_pixel, message
def test_dirty_pixels_rgba(self) -> None: def test_dirty_pixels_rgba(self) -> None:
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))
@ -406,6 +423,7 @@ class TestCoreResampleCoefficients:
draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color)
px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load() px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load()
assert px is not None
if px[2, 0] != test_color // 2: if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0] assert test_color // 2 == px[2, 0]
@ -445,7 +463,7 @@ class TestCoreResampleBox:
im.resize((32, 32), resample, (20, 20, 100, 20)) im.resize((32, 32), resample, (20, 20, 100, 20))
with pytest.raises(TypeError, match="must be sequence of length 4"): with pytest.raises(TypeError, match="must be sequence of length 4"):
im.resize((32, 32), resample, (im.width, im.height)) im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
with pytest.raises(ValueError, match="can't be negative"): with pytest.raises(ValueError, match="can't be negative"):
im.resize((32, 32), resample, (-20, 20, 100, 100)) im.resize((32, 32), resample, (-20, 20, 100, 100))

View File

@ -4,9 +4,9 @@ Tests for resize functionality.
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
from itertools import permutations from itertools import permutations
from pathlib import Path from pathlib import Path
from typing import Generator
import pytest import pytest
@ -285,14 +285,14 @@ class TestReducingGapResize:
class TestImageResize: class TestImageResize:
def test_resize(self) -> None: def test_resize(self) -> None:
def resize(mode: str, size: tuple[int, int]) -> None: def resize(mode: str, size: tuple[int, int] | list[int]) -> None:
out = hopper(mode).resize(size) out = hopper(mode).resize(size)
assert out.mode == mode assert out.mode == mode
assert out.size == size assert out.size == tuple(size)
for mode in "1", "P", "L", "RGB", "I", "F": for mode in "1", "P", "L", "RGB", "I", "F":
resize(mode, (112, 103)) resize(mode, (112, 103))
resize(mode, (188, 214)) resize(mode, [188, 214])
# Test unknown resampling filter # Test unknown resampling filter
with hopper() as im: with hopper() as im:

View File

@ -124,8 +124,8 @@ def test_fastpath_translate() -> None:
def test_center() -> None: def test_center() -> None:
im = hopper() im = hopper()
rotate(im, im.mode, 45, center=(0, 0)) rotate(im, im.mode, 45, center=(0, 0))
rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) rotate(im, im.mode, 45, translate=(im.size[0] // 2, 0))
rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] // 2, 0))
def test_rotate_no_fill() -> None: def test_rotate_no_fill() -> None:

View File

@ -16,7 +16,7 @@ from .helper import (
def test_sanity() -> None: def test_sanity() -> None:
im = hopper() im = hopper()
assert im.thumbnail((100, 100)) is None im.thumbnail((100, 100))
assert im.size == (100, 100) assert im.size == (100, 100)
@ -111,7 +111,9 @@ def test_load_first_unless_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft draft = im.draft
def im_draft(mode: str, size: tuple[int, int]): def im_draft(
mode: str, size: tuple[int, int]
) -> tuple[str, tuple[int, int, float, float]] | None:
result = draft(mode, size) result = draft(mode, size)
assert result is not None assert result is not None

View File

@ -192,8 +192,9 @@ class TestImageTransform:
im = op(im, (40, 10)) im = op(im, (40, 10))
colors = sorted(im.getcolors()) colors = im.getcolors()
assert colors == sorted( assert colors is not None
assert sorted(colors) == sorted(
( (
(20 * 10, opaque), (20 * 10, opaque),
(20 * 10, transparent), (20 * 10, transparent),

View File

@ -391,23 +391,25 @@ def test_overlay() -> None:
def test_logical() -> None: def test_logical() -> None:
def table( def table(
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
) -> tuple[int, int, int, int]: ) -> list[float]:
out = [] out = []
for x in (a, b): for x in (a, b):
imx = Image.new("1", (1, 1), x) imx = Image.new("1", (1, 1), x)
for y in (a, b): for y in (a, b):
imy = Image.new("1", (1, 1), y) imy = Image.new("1", (1, 1), y)
out.append(op(imx, imy).getpixel((0, 0))) value = op(imx, imy).getpixel((0, 0))
return tuple(out) assert not isinstance(value, tuple) and value is not None
out.append(value)
return out
assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255) assert table(ImageChops.logical_and, 0, 1) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255) assert table(ImageChops.logical_or, 0, 1) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0) assert table(ImageChops.logical_xor, 0, 1) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255) assert table(ImageChops.logical_and, 0, 128) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255) assert table(ImageChops.logical_or, 0, 128) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0) assert table(ImageChops.logical_xor, 0, 128) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255) assert table(ImageChops.logical_and, 0, 255) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255) assert table(ImageChops.logical_or, 0, 255) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0) assert table(ImageChops.logical_xor, 0, 255) == [0, 255, 255, 0]

View File

@ -7,7 +7,7 @@ import shutil
import sys import sys
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Literal, cast
import pytest import pytest
@ -60,10 +60,13 @@ def test_sanity() -> None:
assert list(map(type, v)) == [str, str, str, str] assert list(map(type, v)) == [str, str, str, str]
# internal version number # internal version number
assert re.search(r"\d+\.\d+(\.\d+)?$", features.version_module("littlecms2")) version = features.version_module("littlecms2")
assert version is not None
assert re.search(r"\d+\.\d+(\.\d+)?$", version)
skip_missing() skip_missing()
i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) i = ImageCms.profileToProfile(hopper(), SRGB, SRGB)
assert i is not None
assert_image(i, "RGB", (128, 128)) assert_image(i, "RGB", (128, 128))
i = hopper() i = hopper()
@ -72,23 +75,27 @@ def test_sanity() -> None:
t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB")
i = ImageCms.applyTransform(hopper(), t) i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert_image(i, "RGB", (128, 128)) assert_image(i, "RGB", (128, 128))
with hopper() as i: with hopper() as i:
t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB")
ImageCms.applyTransform(hopper(), t, inPlace=True) ImageCms.applyTransform(hopper(), t, inPlace=True)
assert i is not None
assert_image(i, "RGB", (128, 128)) assert_image(i, "RGB", (128, 128))
p = ImageCms.createProfile("sRGB") p = ImageCms.createProfile("sRGB")
o = ImageCms.getOpenProfile(SRGB) o = ImageCms.getOpenProfile(SRGB)
t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB")
i = ImageCms.applyTransform(hopper(), t) i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert_image(i, "RGB", (128, 128)) assert_image(i, "RGB", (128, 128))
t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB")
assert t.inputMode == "RGB" assert t.inputMode == "RGB"
assert t.outputMode == "RGB" assert t.outputMode == "RGB"
i = ImageCms.applyTransform(hopper(), t) i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert_image(i, "RGB", (128, 128)) assert_image(i, "RGB", (128, 128))
# test PointTransform convenience API # test PointTransform convenience API
@ -96,7 +103,7 @@ def test_sanity() -> None:
def test_flags() -> None: def test_flags() -> None:
assert ImageCms.Flags.NONE == 0 assert ImageCms.Flags.NONE.value == 0
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
@ -202,13 +209,13 @@ def test_exceptions() -> None:
ImageCms.buildTransform("foo", "bar", "RGB", "RGB") ImageCms.buildTransform("foo", "bar", "RGB", "RGB")
with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"): with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"):
ImageCms.getProfileName(None) ImageCms.getProfileName(None) # type: ignore[arg-type]
skip_missing() skip_missing()
# Python <= 3.9: "an integer is required (got type NoneType)" # Python <= 3.9: "an integer is required (got type NoneType)"
# Python > 3.9: "'NoneType' object cannot be interpreted as an integer" # Python > 3.9: "'NoneType' object cannot be interpreted as an integer"
with pytest.raises(ImageCms.PyCMSError, match="integer"): with pytest.raises(ImageCms.PyCMSError, match="integer"):
ImageCms.isIntentSupported(SRGB, None, None) ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type]
def test_display_profile() -> None: def test_display_profile() -> None:
@ -232,7 +239,7 @@ def test_unsupported_color_space() -> None:
"Color space not supported for on-the-fly profile creation (unsupported)" "Color space not supported for on-the-fly profile creation (unsupported)"
), ),
): ):
ImageCms.createProfile("unsupported") ImageCms.createProfile("unsupported") # type: ignore[arg-type]
def test_invalid_color_temperature() -> None: def test_invalid_color_temperature() -> None:
@ -240,7 +247,7 @@ def test_invalid_color_temperature() -> None:
ImageCms.PyCMSError, ImageCms.PyCMSError,
match='Color temperature must be numeric, "invalid" not valid', match='Color temperature must be numeric, "invalid" not valid',
): ):
ImageCms.createProfile("LAB", "invalid") ImageCms.createProfile("LAB", "invalid") # type: ignore[arg-type]
@pytest.mark.parametrize("flag", ("my string", -1)) @pytest.mark.parametrize("flag", ("my string", -1))
@ -249,7 +256,7 @@ def test_invalid_flag(flag: str | int) -> None:
with pytest.raises( with pytest.raises(
ImageCms.PyCMSError, match="flags must be an integer between 0 and " ImageCms.PyCMSError, match="flags must be an integer between 0 and "
): ):
ImageCms.profileToProfile(im, "foo", "bar", flags=flag) ImageCms.profileToProfile(im, "foo", "bar", flags=flag) # type: ignore[arg-type]
def test_simple_lab() -> None: def test_simple_lab() -> None:
@ -260,7 +267,7 @@ def test_simple_lab() -> None:
t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB")
i_lab = ImageCms.applyTransform(i, t) i_lab = ImageCms.applyTransform(i, t)
assert i_lab is not None
assert i_lab.mode == "LAB" assert i_lab.mode == "LAB"
k = i_lab.getpixel((0, 0)) k = i_lab.getpixel((0, 0))
@ -284,6 +291,7 @@ def test_lab_color() -> None:
# Need to add a type mapping for some PIL type to TYPE_Lab_8 in findLCMSType, and # Need to add a type mapping for some PIL type to TYPE_Lab_8 in findLCMSType, and
# have that mapping work back to a PIL mode (likely RGB). # have that mapping work back to a PIL mode (likely RGB).
i = ImageCms.applyTransform(hopper(), t) i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert_image(i, "LAB", (128, 128)) assert_image(i, "LAB", (128, 128))
# i.save('temp.lab.tif') # visually verified vs PS. # i.save('temp.lab.tif') # visually verified vs PS.
@ -298,6 +306,7 @@ def test_lab_srgb() -> None:
with Image.open("Tests/images/hopper.Lab.tif") as img: with Image.open("Tests/images/hopper.Lab.tif") as img:
img_srgb = ImageCms.applyTransform(img, t) img_srgb = ImageCms.applyTransform(img, t)
assert img_srgb is not None
# img_srgb.save('temp.srgb.tif') # visually verified vs ps. # img_srgb.save('temp.srgb.tif') # visually verified vs ps.
@ -317,11 +326,11 @@ def test_lab_roundtrip() -> None:
t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
i = ImageCms.applyTransform(hopper(), t) i = ImageCms.applyTransform(hopper(), t)
assert i is not None
assert i.info["icc_profile"] == ImageCmsProfile(pLab).tobytes() assert i.info["icc_profile"] == ImageCmsProfile(pLab).tobytes()
out = ImageCms.applyTransform(i, t2) out = ImageCms.applyTransform(i, t2)
assert out is not None
assert_image_similar(hopper(), out, 2) assert_image_similar(hopper(), out, 2)
@ -343,7 +352,7 @@ def test_extended_information() -> None:
p = o.profile p = o.profile
def assert_truncated_tuple_equal( def assert_truncated_tuple_equal(
tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10 tup1: tuple[Any, ...] | None, tup2: tuple[Any, ...], digits: int = 10
) -> None: ) -> None:
# Helper function to reduce precision of tuples of floats # Helper function to reduce precision of tuples of floats
# recursively and then check equality. # recursively and then check equality.
@ -359,6 +368,7 @@ def test_extended_information() -> None:
for val in tuple_value for val in tuple_value
) )
assert tup1 is not None
assert truncate_tuple(tup1) == truncate_tuple(tup2) assert truncate_tuple(tup1) == truncate_tuple(tup2)
assert p.attributes == 4294967296 assert p.attributes == 4294967296
@ -504,22 +514,22 @@ def test_non_ascii_path(tmp_path: Path) -> None:
def test_profile_typesafety() -> None: def test_profile_typesafety() -> None:
# does not segfault # does not segfault
with pytest.raises(TypeError, match="Invalid type for Profile"): with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(0).tobytes() ImageCms.ImageCmsProfile(0) # type: ignore[arg-type]
with pytest.raises(TypeError, match="Invalid type for Profile"): with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(1).tobytes() ImageCms.ImageCmsProfile(1) # type: ignore[arg-type]
# also check core function # also check core function
with pytest.raises(TypeError): with pytest.raises(TypeError):
ImageCms.core.profile_tobytes(0) ImageCms.core.profile_tobytes(0) # type: ignore[arg-type]
with pytest.raises(TypeError): with pytest.raises(TypeError):
ImageCms.core.profile_tobytes(1) ImageCms.core.profile_tobytes(1) # type: ignore[arg-type]
if not is_pypy(): if not is_pypy():
# core profile should not be directly instantiable # core profile should not be directly instantiable
with pytest.raises(TypeError): with pytest.raises(TypeError):
ImageCms.core.CmsProfile() ImageCms.core.CmsProfile()
with pytest.raises(TypeError): with pytest.raises(TypeError):
ImageCms.core.CmsProfile(0) ImageCms.core.CmsProfile(0) # type: ignore[call-arg]
@pytest.mark.skipif(is_pypy(), reason="fails on PyPy") @pytest.mark.skipif(is_pypy(), reason="fails on PyPy")
@ -528,7 +538,7 @@ def test_transform_typesafety() -> None:
with pytest.raises(TypeError): with pytest.raises(TypeError):
ImageCms.core.CmsTransform() ImageCms.core.CmsTransform()
with pytest.raises(TypeError): with pytest.raises(TypeError):
ImageCms.core.CmsTransform(0) ImageCms.core.CmsTransform(0) # type: ignore[call-arg]
def assert_aux_channel_preserved( def assert_aux_channel_preserved(
@ -559,9 +569,9 @@ def assert_aux_channel_preserved(
for delta in nine_grid_deltas: for delta in nine_grid_deltas:
channel_data.paste( channel_data.paste(
channel_pattern, channel_pattern,
tuple( (
paste_offset[c] + delta[c] * channel_pattern.size[c] paste_offset[0] + delta[0] * channel_pattern.size[0],
for c in range(2) paste_offset[1] + delta[1] * channel_pattern.size[1],
), ),
) )
chans.append(channel_data) chans.append(channel_data)
@ -578,11 +588,13 @@ def assert_aux_channel_preserved(
) )
# apply transform # apply transform
result_image: Image.Image | None
if transform_in_place: if transform_in_place:
ImageCms.applyTransform(source_image, t, inPlace=True) ImageCms.applyTransform(source_image, t, inPlace=True)
result_image = source_image result_image = source_image
else: else:
result_image = ImageCms.applyTransform(source_image, t, inPlace=False) result_image = ImageCms.applyTransform(source_image, t, inPlace=False)
assert result_image is not None
result_image_aux = result_image.getchannel(preserved_channel) result_image_aux = result_image.getchannel(preserved_channel)
assert_image_equal(source_image_aux, result_image_aux) assert_image_equal(source_image_aux, result_image_aux)
@ -628,8 +640,10 @@ def test_auxiliary_channels_isolated() -> None:
continue continue
# convert with and without AUX data, test colors are equal # convert with and without AUX data, test colors are equal
source_profile = ImageCms.createProfile(src_format[1]) src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
destination_profile = ImageCms.createProfile(dst_format[1]) source_profile = ImageCms.createProfile(src_colorSpace)
dst_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], dst_format[1])
destination_profile = ImageCms.createProfile(dst_colorSpace)
source_image = src_format[3] source_image = src_format[3]
test_transform = ImageCms.buildTransform( test_transform = ImageCms.buildTransform(
source_profile, source_profile,
@ -639,6 +653,7 @@ def test_auxiliary_channels_isolated() -> None:
) )
# test conversion from aux-ful source # test conversion from aux-ful source
test_image: Image.Image | None
if transform_in_place: if transform_in_place:
test_image = source_image.copy() test_image = source_image.copy()
ImageCms.applyTransform(test_image, test_transform, inPlace=True) ImageCms.applyTransform(test_image, test_transform, inPlace=True)
@ -646,6 +661,7 @@ def test_auxiliary_channels_isolated() -> None:
test_image = ImageCms.applyTransform( test_image = ImageCms.applyTransform(
source_image, test_transform, inPlace=False source_image, test_transform, inPlace=False
) )
assert test_image is not None
# reference conversion from aux-less source # reference conversion from aux-less source
reference_transform = ImageCms.buildTransform( reference_transform = ImageCms.buildTransform(
@ -657,13 +673,14 @@ def test_auxiliary_channels_isolated() -> None:
reference_image = ImageCms.applyTransform( reference_image = ImageCms.applyTransform(
source_image.convert(src_format[2]), reference_transform source_image.convert(src_format[2]), reference_transform
) )
assert reference_image is not None
assert_image_equal(test_image.convert(dst_format[2]), reference_image) assert_image_equal(test_image.convert(dst_format[2]), reference_image)
def test_long_modes() -> None: def test_long_modes() -> None:
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc") p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI") with pytest.warns(DeprecationWarning):
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
@ -674,7 +691,9 @@ def test_rgb_lab(mode: str) -> None:
im = Image.new("LAB", (1, 1), (255, 0, 0)) im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode) converted_im = im.convert(mode)
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) value = converted_im.getpixel((0, 0))
assert isinstance(value, tuple)
assert value[:3] == (0, 255, 255)
def test_deprecation() -> None: def test_deprecation() -> None:
@ -684,3 +703,9 @@ def test_deprecation() -> None:
assert ImageCms.VERSION == "1.0.0 pil" assert ImageCms.VERSION == "1.0.0 pil"
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert isinstance(ImageCms.FLAGS, dict) assert isinstance(ImageCms.FLAGS, dict)
profile = ImageCmsProfile(ImageCms.createProfile("sRGB"))
with pytest.warns(DeprecationWarning):
ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB")
with pytest.warns(DeprecationWarning):
ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B")

View File

@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import os.path import os.path
from collections.abc import Sequence
from typing import Callable
import pytest import pytest
@ -265,6 +266,21 @@ def test_chord_too_fat() -> None:
assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png")
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("xy", ((W / 2, H / 2), [W / 2, H / 2]))
def test_circle(mode: str, xy: Sequence[float]) -> None:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
expected = f"Tests/images/imagedraw_ellipse_{mode}.png"
# Act
draw.circle(xy, 25, fill="green", outline="blue")
# Assert
assert_image_similar_tofile(im, expected, 1)
@pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(mode: str, bbox: Coords) -> None: def test_ellipse(mode: str, bbox: Coords) -> None:
@ -432,6 +448,7 @@ def test_shape1() -> None:
x3, y3 = 95, 5 x3, y3 = 95, 5
# Act # Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline() s = ImageDraw.Outline()
s.move(x0, y0) s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3) s.curve(x1, y1, x2, y2, x3, y3)
@ -453,6 +470,7 @@ def test_shape2() -> None:
x3, y3 = 5, 95 x3, y3 = 5, 95
# Act # Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline() s = ImageDraw.Outline()
s.move(x0, y0) s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3) s.curve(x1, y1, x2, y2, x3, y3)
@ -471,6 +489,7 @@ def test_transform() -> None:
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline() s = ImageDraw.Outline()
s.line(0, 0) s.line(0, 0)
s.transform((0, 0, 0, 0, 0, 0)) s.transform((0, 0, 0, 0, 0, 0))
@ -615,6 +634,19 @@ def test_polygon(points: Coords) -> None:
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
@pytest.mark.parametrize("points", POINTS)
def test_polygon_width_I16(points: Coords) -> None:
# Arrange
im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.polygon(points, outline=0xFFFF, width=2)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon_width_I.tiff")
@pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("kite_points", KITE_POINTS) @pytest.mark.parametrize("kite_points", KITE_POINTS)
def test_polygon_kite( def test_polygon_kite(
@ -897,7 +929,12 @@ def test_rounded_rectangle_translucent(
def test_floodfill(bbox: Coords) -> None: def test_floodfill(bbox: Coords) -> None:
red = ImageColor.getrgb("red") red = ImageColor.getrgb("red")
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: mode_values: list[tuple[str, int | tuple[int, ...]]] = [
("L", 1),
("RGBA", (255, 0, 0, 0)),
("RGB", red),
]
for mode, value in mode_values:
# Arrange # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -1067,8 +1104,8 @@ def test_line_horizontal() -> None:
) )
@pytest.mark.xfail(reason="failing test")
def test_line_h_s1_w2() -> None: def test_line_h_s1_w2() -> None:
pytest.skip("failing")
img, draw = create_base_image_draw((20, 20)) img, draw = create_base_image_draw((20, 20))
draw.line((5, 5, 14, 6), BLACK, 2) draw.line((5, 5, 14, 6), BLACK, 2)
assert_image_equal_tofile( assert_image_equal_tofile(
@ -1385,25 +1422,44 @@ def test_default_font_size() -> None:
im = Image.new("RGB", (220, 25)) im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def check(func: Callable[[], None]) -> None:
if freetype_support:
func()
else:
with pytest.raises(ImportError):
func()
def draw_text() -> None:
draw.text((0, 0), text, font_size=16) draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): check(draw_text)
def draw_textlength() -> None:
assert draw.textlength(text, font_size=16) == 216 assert draw.textlength(text, font_size=16) == 216
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): check(draw_textlength)
def draw_textbbox() -> None:
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_textbbox)
im = Image.new("RGB", (220, 25)) im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def draw_multiline_text() -> None:
draw.multiline_text((0, 0), text, font_size=16) draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): check(draw_multiline_text)
def draw_multiline_textbbox() -> None:
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_multiline_textbbox)
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox: Coords) -> None: def test_same_color_outline(bbox: Coords) -> None:
@ -1413,6 +1469,7 @@ def test_same_color_outline(bbox: Coords) -> None:
x2, y2 = 95, 50 x2, y2 = 95, 50
x3, y3 = 95, 5 x3, y3 = 95, 5
assert ImageDraw.Outline is not None
s = ImageDraw.Outline() s = ImageDraw.Outline()
s.move(x0, y0) s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3) s.curve(x1, y1, x2, y2, x3, y3)
@ -1451,7 +1508,7 @@ def test_same_color_outline(bbox: Coords) -> None:
(4, "square", {}), (4, "square", {}),
(8, "regular_octagon", {}), (8, "regular_octagon", {}),
(4, "square_rotate_45", {"rotation": 45}), (4, "square_rotate_45", {"rotation": 45}),
(3, "triangle_width", {"width": 5, "outline": "yellow"}), (3, "triangle_width", {"outline": "yellow", "width": 5}),
], ],
) )
def test_draw_regular_polygon( def test_draw_regular_polygon(
@ -1461,7 +1518,10 @@ def test_draw_regular_polygon(
filename = f"Tests/images/imagedraw_{polygon_name}.png" filename = f"Tests/images/imagedraw_{polygon_name}.png"
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
bounding_circle = ((W // 2, H // 2), 25) bounding_circle = ((W // 2, H // 2), 25)
draw.regular_polygon(bounding_circle, n_sides, fill="red", **args) rotation = int(args.get("rotation", 0))
outline = args.get("outline")
width = int(args.get("width", 1))
draw.regular_polygon(bounding_circle, n_sides, rotation, "red", outline, width)
assert_image_equal_tofile(im, filename) assert_image_equal_tofile(im, filename)
@ -1546,10 +1606,14 @@ def test_compute_regular_polygon_vertices(
], ],
) )
def test_compute_regular_polygon_vertices_input_error_handling( def test_compute_regular_polygon_vertices_input_error_handling(
n_sides, bounding_circle, rotation, expected_error, error_message n_sides: int,
bounding_circle: int | tuple[int | tuple[int] | str, ...],
rotation: int | str,
expected_error: type[Exception],
error_message: str,
) -> None: ) -> None:
with pytest.raises(expected_error) as e: with pytest.raises(expected_error) as e:
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
assert str(e.value) == error_message assert str(e.value) == error_message
@ -1608,3 +1672,8 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
draw.rectangle(xy) draw.rectangle(xy)
with pytest.raises(ValueError): with pytest.raises(ValueError):
draw.rounded_rectangle(xy) draw.rounded_rectangle(xy)
def test_getdraw() -> None:
with pytest.warns(DeprecationWarning):
ImageDraw.getdraw(None, [])

View File

@ -51,9 +51,18 @@ def test_sanity() -> None:
pen = ImageDraw2.Pen("blue", width=7) pen = ImageDraw2.Pen("blue", width=7)
draw.line(list(range(10)), pen) draw.line(list(range(10)), pen)
draw, handler = ImageDraw.getdraw(im) draw2, handler = ImageDraw.getdraw(im)
assert draw2 is not None
pen = ImageDraw2.Pen("blue", width=7) pen = ImageDraw2.Pen("blue", width=7)
draw.line(list(range(10)), pen) draw2.line(list(range(10)), pen)
def test_mode() -> None:
draw = ImageDraw2.Draw("L", (1, 1))
assert draw.image.mode == "L"
with pytest.raises(ValueError):
ImageDraw2.Draw("L")
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)

View File

@ -90,6 +90,7 @@ class TestImageFile:
data = f.read() data = f.read()
with ImageFile.Parser() as p: with ImageFile.Parser() as p:
p.feed(data) p.feed(data)
assert p.image is not None
assert (48, 48) == p.image.size assert (48, 48) == p.image.size
@skip_unless_feature("webp") @skip_unless_feature("webp")
@ -103,6 +104,7 @@ class TestImageFile:
assert not p.image assert not p.image
p.feed(f.read()) p.feed(f.read())
assert p.image is not None
assert (128, 128) == p.image.size assert (128, 128) == p.image.size
@skip_unless_feature("zlib") @skip_unless_feature("zlib")
@ -125,7 +127,7 @@ class TestImageFile:
def test_raise_typeerror(self) -> None: def test_raise_typeerror(self) -> None:
with pytest.raises(TypeError): with pytest.raises(TypeError):
parser = ImageFile.Parser() parser = ImageFile.Parser()
parser.feed(1) parser.feed(1) # type: ignore[arg-type]
def test_negative_stride(self) -> None: def test_negative_stride(self) -> None:
with open("Tests/images/raw_negative_stride.bin", "rb") as f: with open("Tests/images/raw_negative_stride.bin", "rb") as f:
@ -202,23 +204,27 @@ class TestImageFile:
class MockPyDecoder(ImageFile.PyDecoder): class MockPyDecoder(ImageFile.PyDecoder):
last: MockPyDecoder
def __init__(self, mode: str, *args: Any) -> None: def __init__(self, mode: str, *args: Any) -> None:
MockPyDecoder.last = self MockPyDecoder.last = self
super().__init__(mode, *args) super().__init__(mode, *args)
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
# eof # eof
return -1, 0 return -1, 0
class MockPyEncoder(ImageFile.PyEncoder): class MockPyEncoder(ImageFile.PyEncoder):
last: MockPyEncoder | None
def __init__(self, mode: str, *args: Any) -> None: def __init__(self, mode: str, *args: Any) -> None:
MockPyEncoder.last = self MockPyEncoder.last = self
super().__init__(mode, *args) super().__init__(mode, *args)
def encode(self, buffer): def encode(self, bufsize: int) -> tuple[int, int, bytes]:
return 1, 1, b"" return 1, 1, b""
def cleanup(self) -> None: def cleanup(self) -> None:
@ -299,9 +305,9 @@ class TestPyDecoder(CodecsTest):
im.load() im.load()
def test_decode(self) -> None: def test_decode(self) -> None:
decoder = ImageFile.PyDecoder(None) decoder = ImageFile.PyDecoder("")
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
decoder.decode(None) decoder.decode(b"")
class TestPyEncoder(CodecsTest): class TestPyEncoder(CodecsTest):
@ -315,6 +321,7 @@ class TestPyEncoder(CodecsTest):
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
) )
assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == xoff assert MockPyEncoder.last.state.xoff == xoff
assert MockPyEncoder.last.state.yoff == yoff assert MockPyEncoder.last.state.yoff == yoff
assert MockPyEncoder.last.state.xsize == xsize assert MockPyEncoder.last.state.xsize == xsize
@ -329,6 +336,7 @@ class TestPyEncoder(CodecsTest):
fp = BytesIO() fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == 0 assert MockPyEncoder.last.state.xoff == 0
assert MockPyEncoder.last.state.yoff == 0 assert MockPyEncoder.last.state.yoff == 0
assert MockPyEncoder.last.state.xsize == 200 assert MockPyEncoder.last.state.xsize == 200
@ -345,7 +353,9 @@ class TestPyEncoder(CodecsTest):
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
) )
assert MockPyEncoder.last.cleanup_called last: MockPyEncoder | None = MockPyEncoder.last
assert last
assert last.cleanup_called
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
@ -373,9 +383,9 @@ class TestPyEncoder(CodecsTest):
) )
def test_encode(self) -> None: def test_encode(self) -> None:
encoder = ImageFile.PyEncoder(None) encoder = ImageFile.PyEncoder("")
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode(None) encoder.encode(0)
bytes_consumed, errcode = encoder.encode_to_pyfd() bytes_consumed, errcode = encoder.encode_to_pyfd()
assert bytes_consumed == 0 assert bytes_consumed == 0
@ -385,8 +395,9 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd() encoder.encode_to_pyfd()
fh = BytesIO()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None) encoder.encode_to_file(fh, 0)
def test_zero_height(self) -> None: def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):

View File

@ -34,7 +34,9 @@ pytestmark = skip_unless_feature("freetype2")
def test_sanity() -> None: def test_sanity() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) version = features.version_module("freetype2")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@pytest.fixture( @pytest.fixture(
@ -207,7 +209,7 @@ def test_getlength(
assert length == length_raqm assert length == length_raqm
def test_float_size() -> None: def test_float_size(layout_engine: ImageFont.Layout) -> None:
lengths = [] lengths = []
for size in (48, 48.5, 49): for size in (48, 48.5, 49):
f = ImageFont.truetype( f = ImageFont.truetype(
@ -222,7 +224,7 @@ def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
line_spacing = font.getbbox("A")[3] + 4 line_spacing = font.getbbox("A")[3] + 4
lines = TEST_TEXT.split("\n") lines = TEST_TEXT.split("\n")
y = 0 y: float = 0
for line in lines: for line in lines:
draw.text((0, y), line, font=font) draw.text((0, y), line, font=font)
y += line_spacing y += line_spacing
@ -492,8 +494,8 @@ def test_default_font() -> None:
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png") assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
@pytest.mark.parametrize("mode", (None, "1", "RGBA")) @pytest.mark.parametrize("mode", ("", "1", "RGBA"))
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
assert (0, 4, 12, 16) == font.getbbox("A", mode) assert (0, 4, 12, 16) == font.getbbox("A", mode)
@ -546,12 +548,11 @@ def test_find_font(
def loadable_font( def loadable_font(
filepath: str, size: int, index: int, encoding: str, *args: Any filepath: str, size: int, index: int, encoding: str, *args: Any
): ) -> ImageFont.FreeTypeFont:
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
if filepath == path_to_fake: if filepath == path_to_fake:
return ImageFont._FreeTypeFont( return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
FONT_PATH, size, index, encoding, *args return _freeTypeFont(filepath, size, index, encoding, *args)
)
return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args)
m.setattr(ImageFont, "FreeTypeFont", loadable_font) m.setattr(ImageFont, "FreeTypeFont", loadable_font)
font = ImageFont.truetype(fontname) font = ImageFont.truetype(fontname)
@ -563,6 +564,7 @@ def test_find_font(
# catching syntax like errors # catching syntax like errors
monkeypatch.setattr(sys, "platform", platform) monkeypatch.setattr(sys, "platform", platform)
if platform == "linux": if platform == "linux":
monkeypatch.setenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
@ -630,7 +632,9 @@ def test_complex_font_settings() -> None:
def test_variation_get(font: ImageFont.FreeTypeFont) -> None: def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2")) version = features.version_module("freetype2")
assert version is not None
freetype = parse_version(version)
if freetype < parse_version("2.9.1"): if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
font.get_variation_names() font.get_variation_names()
@ -700,7 +704,9 @@ def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None
def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2")) version = features.version_module("freetype2")
assert version is not None
freetype = parse_version(version)
if freetype < parse_version("2.9.1"): if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
font.set_variation_by_name("Bold") font.set_variation_by_name("Bold")
@ -725,7 +731,9 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None:
freetype = parse_version(features.version_module("freetype2")) version = features.version_module("freetype2")
assert version is not None
freetype = parse_version(version)
if freetype < parse_version("2.9.1"): if freetype < parse_version("2.9.1"):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
font.set_variation_by_axes([100]) font.set_variation_by_axes([100])
@ -1089,6 +1097,23 @@ def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
imagefont.getmask("A" * 1_000_001) imagefont.getmask("A" * 1_000_001)
def test_bytes(font: ImageFont.FreeTypeFont) -> None:
assert font.getlength(b"test") == font.getlength("test")
assert font.getbbox(b"test") == font.getbbox("test")
assert_image_equal(
Image.Image()._new(font.getmask(b"test")),
Image.Image()._new(font.getmask("test")),
)
assert_image_equal(
Image.Image()._new(font.getmask2(b"test")[0]),
Image.Image()._new(font.getmask2("test")[0]),
)
assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file",
[ [

View File

@ -9,51 +9,57 @@ from PIL import Image, ImageDraw, ImageFont, _util, features
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
original_core = ImageFont.core fonts = [ImageFont.load_default_imagefont()]
if not features.check_module("freetype2"):
default_font = ImageFont.load_default()
if isinstance(default_font, ImageFont.ImageFont):
fonts.append(default_font)
def setup_module() -> None: @pytest.mark.parametrize("font", fonts)
if features.check_module("freetype2"): def test_default_font(font: ImageFont.ImageFont) -> None:
ImageFont.core = _util.DeferredError(ImportError)
def teardown_module() -> None:
ImageFont.core = original_core
def test_default_font() -> None:
# Arrange # Arrange
txt = 'This is a "better than nothing" default font.' txt = 'This is a "better than nothing" default font.'
im = Image.new(mode="RGB", size=(300, 100)) im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
default_font = ImageFont.load_default() draw.text((10, 10), txt, font=font)
draw.text((10, 10), txt, font=default_font)
# Assert # Assert
assert_image_equal_tofile(im, "Tests/images/default_font.png") assert_image_equal_tofile(im, "Tests/images/default_font.png")
def test_size_without_freetype() -> None: def test_without_freetype() -> None:
with pytest.raises(ImportError): original_core = ImageFont.core
ImageFont.load_default(size=14) if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try:
with pytest.raises(ImportError):
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
assert isinstance(ImageFont.load_default(), ImageFont.ImageFont)
with pytest.raises(ImportError):
ImageFont.load_default(size=14)
finally:
ImageFont.core = original_core
def test_unicode() -> None: @pytest.mark.parametrize("font", fonts)
def test_unicode(font: ImageFont.ImageFont) -> None:
# should not segfault, should return UnicodeDecodeError # should not segfault, should return UnicodeDecodeError
# issue #2826 # issue #2826
font = ImageFont.load_default()
with pytest.raises(UnicodeEncodeError): with pytest.raises(UnicodeEncodeError):
font.getbbox("") font.getbbox("")
def test_textbbox() -> None: @pytest.mark.parametrize("font", fonts)
def test_textbbox(font: ImageFont.ImageFont) -> None:
im = Image.new("RGB", (200, 200)) im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im) d = ImageDraw.Draw(im)
default_font = ImageFont.load_default() assert d.textlength("test", font=font) == 24
assert d.textlength("test", font=default_font) == 24 assert d.textbbox((0, 0), "test", font=font) == (0, 0, 24, 11)
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
def test_decompression_bomb() -> None: def test_decompression_bomb() -> None:

View File

@ -60,6 +60,8 @@ class TestImageGrab:
def test_grabclipboard(self) -> None: def test_grabclipboard(self) -> None:
if sys.platform == "darwin": if sys.platform == "darwin":
subprocess.call(["screencapture", "-cx"]) subprocess.call(["screencapture", "-cx"])
ImageGrab.grabclipboard()
elif sys.platform == "win32": elif sys.platform == "win32":
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
p.stdin.write( p.stdin.write(
@ -69,6 +71,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200
[Windows.Forms.Clipboard]::SetImage($bmp)""" [Windows.Forms.Clipboard]::SetImage($bmp)"""
) )
p.communicate() p.communicate()
ImageGrab.grabclipboard()
else: else:
if not shutil.which("wl-paste") and not shutil.which("xclip"): if not shutil.which("wl-paste") and not shutil.which("xclip"):
with pytest.raises( with pytest.raises(
@ -77,9 +81,6 @@ $bmp = New-Object Drawing.Bitmap 200, 200
r" ImageGrab.grabclipboard\(\) on Linux", r" ImageGrab.grabclipboard\(\) on Linux",
): ):
ImageGrab.grabclipboard() ImageGrab.grabclipboard()
return
ImageGrab.grabclipboard()
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None: def test_grabclipboard_file(self) -> None:
@ -89,6 +90,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
p.communicate() p.communicate()
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert isinstance(im, list)
assert len(im) == 1 assert len(im) == 1
assert os.path.samefile(im[0], "Tests/images/hopper.gif") assert os.path.samefile(im[0], "Tests/images/hopper.gif")
@ -105,6 +107,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
p.communicate() p.communicate()
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, "Tests/images/hopper.png") assert_image_equal_tofile(im, "Tests/images/hopper.png")
@pytest.mark.skipif( @pytest.mark.skipif(
@ -120,6 +123,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
with open(image_path, "rb") as fp: with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp) subprocess.call(["wl-copy"], stdin=fp)
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert isinstance(im, Image.Image)
assert_image_equal_tofile(im, image_path) assert_image_equal_tofile(im, image_path)
@pytest.mark.skipif( @pytest.mark.skipif(

View File

@ -1,5 +1,9 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import pytest
from PIL import Image, ImageMath from PIL import Image, ImageMath
@ -19,7 +23,7 @@ I = Image.new("I", (1, 1), 4) # noqa: E741
A2 = A.resize((2, 2)) A2 = A.resize((2, 2))
B2 = B.resize((2, 2)) B2 = B.resize((2, 2))
images = {"A": A, "B": B, "F": F, "I": I} images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I}
def test_sanity() -> None: def test_sanity() -> None:
@ -30,13 +34,13 @@ def test_sanity() -> None:
== "I 3" == "I 3"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images))
== "I 3" == "I 3"
) )
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) + args["B"], images lambda args: args["float"](args["A"]) + args["B"], **images
) )
) )
== "F 3.0" == "F 3.0"
@ -44,42 +48,47 @@ def test_sanity() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["int"](args["float"](args["A"]) + args["B"]), images lambda args: args["int"](args["float"](args["A"]) + args["B"]), **images
) )
) )
== "I 3" == "I 3"
) )
def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.lambda_eval(lambda args: 1, images) == 1
def test_ops() -> None: def test_ops() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1" assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1"
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images))
== "I 3" == "I 3"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], **images))
== "I -1" == "I -1"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], **images))
== "I 2" == "I 2"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], **images))
== "I 0" == "I 0"
) )
assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4" assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, **images)) == "I 4"
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images)) pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, **images))
== "I 2147483647" == "I 2147483647"
) )
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) + args["B"], images lambda args: args["float"](args["A"]) + args["B"], **images
) )
) )
== "F 3.0" == "F 3.0"
@ -87,7 +96,7 @@ def test_ops() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) - args["B"], images lambda args: args["float"](args["A"]) - args["B"], **images
) )
) )
== "F -1.0" == "F -1.0"
@ -95,7 +104,7 @@ def test_ops() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) * args["B"], images lambda args: args["float"](args["A"]) * args["B"], **images
) )
) )
== "F 2.0" == "F 2.0"
@ -103,31 +112,33 @@ def test_ops() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) / args["B"], images lambda args: args["float"](args["A"]) / args["B"], **images
) )
) )
== "F 0.5" == "F 0.5"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images)) pixel(
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, **images)
)
== "F 4.0" == "F 4.0"
) )
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images) ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, **images)
) )
== "F 8589934592.0" == "F 8589934592.0"
) )
def test_logical() -> None: def test_logical() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0 assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], **images)) == 0
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], **images))
== "L 2" == "L 2"
) )
assert ( assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images)) pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], **images))
== "L 1" == "L 1"
) )
@ -136,7 +147,7 @@ def test_convert() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "L"), images lambda args: args["convert"](args["A"] + args["B"], "L"), **images
) )
) )
== "L 3" == "L 3"
@ -144,7 +155,7 @@ def test_convert() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "1"), images lambda args: args["convert"](args["A"] + args["B"], "1"), **images
) )
) )
== "1 0" == "1 0"
@ -152,7 +163,7 @@ def test_convert() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "RGB"), images lambda args: args["convert"](args["A"] + args["B"], "RGB"), **images
) )
) )
== "RGB (3, 3, 3)" == "RGB (3, 3, 3)"
@ -163,7 +174,7 @@ def test_compare() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["min"](args["A"], args["B"]), images lambda args: args["min"](args["A"], args["B"]), **images
) )
) )
== "I 1" == "I 1"
@ -171,13 +182,13 @@ def test_compare() -> None:
assert ( assert (
pixel( pixel(
ImageMath.lambda_eval( ImageMath.lambda_eval(
lambda args: args["max"](args["A"], args["B"]), images lambda args: args["max"](args["A"], args["B"]), **images
) )
) )
== "I 2" == "I 2"
) )
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1" assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, **images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0" assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, **images)) == "I 0"
def test_one_image_larger() -> None: def test_one_image_larger() -> None:

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import pytest import pytest
from PIL import Image, ImageMath from PIL import Image, ImageMath
@ -21,16 +23,16 @@ I = Image.new("I", (1, 1), 4) # noqa: E741
A2 = A.resize((2, 2)) A2 = A.resize((2, 2))
B2 = B.resize((2, 2)) B2 = B.resize((2, 2))
images = {"A": A, "B": B, "F": F, "I": I} images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I}
def test_sanity() -> None: def test_sanity() -> None:
assert ImageMath.unsafe_eval("1") == 1 assert ImageMath.unsafe_eval("1") == 1
assert ImageMath.unsafe_eval("1+A", A=2) == 3 assert ImageMath.unsafe_eval("1+A", A=2) == 3
assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3" assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3" assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3"
def test_eval_deprecated() -> None: def test_eval_deprecated() -> None:
@ -38,23 +40,28 @@ def test_eval_deprecated() -> None:
assert ImageMath.eval("1") == 1 assert ImageMath.eval("1") == 1
def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.unsafe_eval("1", images) == 1
def test_ops() -> None: def test_ops() -> None:
assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1" assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2" assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1" assert pixel(ImageMath.unsafe_eval("A-B", **images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2" assert pixel(ImageMath.unsafe_eval("A*B", **images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0" assert pixel(ImageMath.unsafe_eval("A/B", **images)) == "I 0"
assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4" assert pixel(ImageMath.unsafe_eval("B**2", **images)) == "I 4"
assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647" assert pixel(ImageMath.unsafe_eval("B**33", **images)) == "I 2147483647"
assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0" assert pixel(ImageMath.unsafe_eval("float(A)-B", **images)) == "F -1.0"
assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0" assert pixel(ImageMath.unsafe_eval("float(A)*B", **images)) == "F 2.0"
assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5" assert pixel(ImageMath.unsafe_eval("float(A)/B", **images)) == "F 0.5"
assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0" assert pixel(ImageMath.unsafe_eval("float(B)**2", **images)) == "F 4.0"
assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0" assert pixel(ImageMath.unsafe_eval("float(B)**33", **images)) == "F 8589934592.0"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -72,33 +79,33 @@ def test_prevent_exec(expression: str) -> None:
def test_prevent_double_underscores() -> None: def test_prevent_double_underscores() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageMath.unsafe_eval("1", {"__": None}) ImageMath.unsafe_eval("1", __=None)
def test_prevent_builtins() -> None: def test_prevent_builtins() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None}) ImageMath.unsafe_eval("(lambda: exec('exit()'))()", exec=None)
def test_logical() -> None: def test_logical() -> None:
assert pixel(ImageMath.unsafe_eval("not A", images)) == 0 assert pixel(ImageMath.unsafe_eval("not A", **images)) == 0
assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2" assert pixel(ImageMath.unsafe_eval("A and B", **images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1" assert pixel(ImageMath.unsafe_eval("A or B", **images)) == "L 1"
def test_convert() -> None: def test_convert() -> None:
assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3" assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", **images)) == "L 3"
assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0" assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", **images)) == "1 0"
assert ( assert (
pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", **images)) == "RGB (3, 3, 3)"
) )
def test_compare() -> None: def test_compare() -> None:
assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1" assert pixel(ImageMath.unsafe_eval("min(A, B)", **images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2" assert pixel(ImageMath.unsafe_eval("max(A, B)", **images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1" assert pixel(ImageMath.unsafe_eval("A == 1", **images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0" assert pixel(ImageMath.unsafe_eval("A == 2", **images)) == "I 0"
def test_one_image_larger() -> None: def test_one_image_larger() -> None:

View File

@ -41,11 +41,15 @@ A = string_to_img(
def img_to_string(im: Image.Image) -> str: def img_to_string(im: Image.Image) -> str:
"""Turn a (small) binary image into a string representation""" """Turn a (small) binary image into a string representation"""
chars = ".1" chars = ".1"
width, height = im.size result = []
return "\n".join( for r in range(im.height):
"".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) line = ""
for r in range(height) for c in range(im.width):
) value = im.getpixel((c, r))
assert not isinstance(value, tuple) and value is not None
line += chars[value > 0]
result.append(line)
return "\n".join(result)
def img_string_normalize(im: str) -> str: def img_string_normalize(im: str) -> str:

View File

@ -165,10 +165,14 @@ def test_pad() -> None:
def test_pad_round() -> None: def test_pad_round() -> None:
im = Image.new("1", (1, 1), 1) im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 1)) new_im = ImageOps.pad(im, (4, 1))
assert new_im.load()[2, 0] == 1 px = new_im.load()
assert px is not None
assert px[2, 0] == 1
new_im = ImageOps.pad(im, (1, 4)) new_im = ImageOps.pad(im, (1, 4))
assert new_im.load()[0, 2] == 1 px = new_im.load()
assert px is not None
assert px[0, 2] == 1
@pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("mode", ("P", "PA"))
@ -223,6 +227,7 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
else: else:
left, top, right, bottom = border left, top, right, bottom = border
px = im_expanded.convert("RGB").load() px = im_expanded.convert("RGB").load()
assert px is not None
for x in range(im_expanded.width): for x in range(im_expanded.width):
for b in range(top): for b in range(top):
assert px[x, b] == (255, 0, 0) assert px[x, b] == (255, 0, 0)
@ -254,20 +259,26 @@ def test_colorize_2color() -> None:
left = (0, 1) left = (0, 1)
middle = (127, 1) middle = (127, 1)
right = (255, 1) right = (255, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(left), value,
(255, 0, 0), (255, 0, 0),
threshold=1, threshold=1,
msg="black test pixel incorrect", msg="black test pixel incorrect",
) )
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(middle), value,
(127, 63, 0), (127, 63, 0),
threshold=1, threshold=1,
msg="mid test pixel incorrect", msg="mid test pixel incorrect",
) )
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(right), value,
(0, 127, 0), (0, 127, 0),
threshold=1, threshold=1,
msg="white test pixel incorrect", msg="white test pixel incorrect",
@ -290,20 +301,26 @@ def test_colorize_2color_offset() -> None:
left = (25, 1) left = (25, 1)
middle = (75, 1) middle = (75, 1)
right = (125, 1) right = (125, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(left), value,
(255, 0, 0), (255, 0, 0),
threshold=1, threshold=1,
msg="black test pixel incorrect", msg="black test pixel incorrect",
) )
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(middle), value,
(127, 63, 0), (127, 63, 0),
threshold=1, threshold=1,
msg="mid test pixel incorrect", msg="mid test pixel incorrect",
) )
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(right), value,
(0, 127, 0), (0, 127, 0),
threshold=1, threshold=1,
msg="white test pixel incorrect", msg="white test pixel incorrect",
@ -334,29 +351,37 @@ def test_colorize_3color_offset() -> None:
middle = (100, 1) middle = (100, 1)
right_middle = (150, 1) right_middle = (150, 1)
right = (225, 1) right = (225, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(left), value,
(255, 0, 0), (255, 0, 0),
threshold=1, threshold=1,
msg="black test pixel incorrect", msg="black test pixel incorrect",
) )
value = im_test.getpixel(left_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(left_middle), value,
(127, 0, 127), (127, 0, 127),
threshold=1, threshold=1,
msg="low-mid test pixel incorrect", msg="low-mid test pixel incorrect",
) )
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect")
value = im_test.getpixel(right_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect" value,
)
assert_tuple_approx_equal(
im_test.getpixel(right_middle),
(0, 63, 127), (0, 63, 127),
threshold=1, threshold=1,
msg="high-mid test pixel incorrect", msg="high-mid test pixel incorrect",
) )
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal( assert_tuple_approx_equal(
im_test.getpixel(right), value,
(0, 127, 0), (0, 127, 0),
threshold=1, threshold=1,
msg="white test pixel incorrect", msg="white test pixel incorrect",
@ -432,6 +457,17 @@ def test_exif_transpose() -> None:
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_xml_without_xmp() -> None:
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
assert im.getexif()[0x0112] == 3
assert "XML:com.adobe.xmp" in im.info
del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_in_place() -> None: def test_exif_transpose_in_place() -> None:
with Image.open("Tests/images/orientation_rectangle.jpg") as im: with Image.open("Tests/images/orientation_rectangle.jpg") as im:
assert im.size == (2, 1) assert im.size == (2, 1)
@ -454,7 +490,7 @@ def test_autocontrast_cutoff() -> None:
# Test the cutoff argument of autocontrast # Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img: with Image.open("Tests/images/bw_gradient.png") as img:
def autocontrast(cutoff: int | tuple[int, int]): def autocontrast(cutoff: int | tuple[int, int]) -> list[int]:
return ImageOps.autocontrast(img, cutoff).histogram() return ImageOps.autocontrast(img, cutoff).histogram()
assert autocontrast(10) == autocontrast((10, 10)) assert autocontrast(10) == autocontrast((10, 10))

View File

@ -1,14 +1,14 @@
from __future__ import annotations from __future__ import annotations
from typing import Generator from collections.abc import Generator
import pytest import pytest
from PIL import Image, ImageFilter from PIL import Image, ImageFile, ImageFilter
@pytest.fixture @pytest.fixture
def test_images() -> Generator[dict[str, Image.Image], None, None]: def test_images() -> Generator[dict[str, ImageFile.ImageFile], None, None]:
ims = { ims = {
"im": Image.open("Tests/images/hopper.ppm"), "im": Image.open("Tests/images/hopper.ppm"),
"snakes": Image.open("Tests/images/color_snakes.png"), "snakes": Image.open("Tests/images/color_snakes.png"),
@ -20,7 +20,7 @@ def test_images() -> Generator[dict[str, Image.Image], None, None]:
im.close() im.close()
def test_filter_api(test_images: dict[str, Image.Image]) -> None: def test_filter_api(test_images: dict[str, ImageFile.ImageFile]) -> None:
im = test_images["im"] im = test_images["im"]
test_filter = ImageFilter.GaussianBlur(2.0) test_filter = ImageFilter.GaussianBlur(2.0)
@ -34,7 +34,7 @@ def test_filter_api(test_images: dict[str, Image.Image]) -> None:
assert i.size == (128, 128) assert i.size == (128, 128)
def test_usm_formats(test_images: dict[str, Image.Image]) -> None: def test_usm_formats(test_images: dict[str, ImageFile.ImageFile]) -> None:
im = test_images["im"] im = test_images["im"]
usm = ImageFilter.UnsharpMask usm = ImageFilter.UnsharpMask
@ -52,13 +52,12 @@ def test_usm_formats(test_images: dict[str, Image.Image]) -> None:
im.convert("YCbCr").filter(usm) im.convert("YCbCr").filter(usm)
def test_blur_formats(test_images: dict[str, Image.Image]) -> None: def test_blur_formats(test_images: dict[str, ImageFile.ImageFile]) -> None:
im = test_images["im"] im = test_images["im"]
blur = ImageFilter.GaussianBlur blur = ImageFilter.GaussianBlur
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.convert("1").filter(blur) im.convert("1").filter(blur)
blur(im.convert("L"))
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.convert("I").filter(blur) im.convert("I").filter(blur)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -70,7 +69,7 @@ def test_blur_formats(test_images: dict[str, Image.Image]) -> None:
im.convert("YCbCr").filter(blur) im.convert("YCbCr").filter(blur)
def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: def test_usm_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
snakes = test_images["snakes"] snakes = test_images["snakes"]
src = snakes.convert("RGB") src = snakes.convert("RGB")
@ -79,7 +78,7 @@ def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None:
assert i.tobytes() == src.tobytes() assert i.tobytes() == src.tobytes()
def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None: def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
snakes = test_images["snakes"] snakes = test_images["snakes"]
i = snakes.filter(ImageFilter.GaussianBlur(0.4)) i = snakes.filter(ImageFilter.GaussianBlur(0.4))
@ -102,7 +101,7 @@ def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None:
assert i.im.getpixel((x, y))[c] >= 250 assert i.im.getpixel((x, y))[c] >= 250
# Fuzzy match. # Fuzzy match.
def gp(x, y): def gp(x: int, y: int) -> tuple[int, ...]:
return i.im.getpixel((x, y)) return i.im.getpixel((x, y))
assert 236 <= gp(7, 4)[0] <= 239 assert 236 <= gp(7, 4)[0] <= 239

View File

@ -45,7 +45,7 @@ def test_getcolor() -> None:
# Test unknown color specifier # Test unknown color specifier
with pytest.raises(ValueError): with pytest.raises(ValueError):
palette.getcolor("unknown") palette.getcolor("unknown") # type: ignore[arg-type]
def test_getcolor_rgba_color_rgb_palette() -> None: def test_getcolor_rgba_color_rgb_palette() -> None:
@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None:
palette.save(f) palette.save(f)
p = ImagePalette.load(f) lut = ImagePalette.load(f)
# load returns raw palette information # load returns raw palette information
assert len(p[0]) == 768 assert len(lut[0]) == 768
assert p[1] == "RGB" assert lut[1] == "RGB"
p = ImagePalette.raw(p[1], p[0]) p = ImagePalette.raw(lut[1], lut[0])
assert isinstance(p, ImagePalette.ImagePalette) assert isinstance(p, ImagePalette.ImagePalette)
assert p.palette == palette.tobytes() assert p.palette == palette.tobytes()

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import array import array
import math import math
import struct import struct
from typing import Sequence from collections.abc import Sequence
import pytest import pytest

View File

@ -41,18 +41,13 @@ def test_rgb() -> None:
checkrgb(0, 0, 255) checkrgb(0, 0, 255)
def test_image() -> None: @pytest.mark.parametrize("mode", ("1", "RGB", "RGBA", "L", "P", "I;16"))
modes = ["1", "RGB", "RGBA", "L", "P"] def test_image(mode: str) -> None:
qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage im = hopper(mode)
if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
modes.append("I;16") if mode not in ("RGB", "RGBA"):
im = im.convert("RGB")
for mode in modes: assert_image_similar(roundtripped_im, im, 1)
im = hopper(mode)
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
if mode not in ("RGB", "RGBA"):
im = im.convert("RGB")
assert_image_similar(roundtripped_im, im, 1)
def test_closed_file() -> None: def test_closed_file() -> None:

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import os
from typing import Any from typing import Any
import pytest import pytest
@ -15,8 +16,11 @@ def test_sanity() -> None:
def test_register() -> None: def test_register() -> None:
# Test registering a viewer that is not a class # Test registering a viewer that is an instance
ImageShow.register("not a class") class TestViewer(ImageShow.Viewer):
pass
ImageShow.register(TestViewer())
# Restore original state # Restore original state
ImageShow._viewers.pop() ImageShow._viewers.pop()
@ -65,6 +69,27 @@ def test_show_without_viewers() -> None:
ImageShow._viewers = viewers ImageShow._viewers = viewers
@pytest.mark.parametrize(
"viewer",
(
ImageShow.Viewer(),
ImageShow.WindowsViewer(),
ImageShow.MacViewer(),
ImageShow.XDGViewer(),
ImageShow.DisplayViewer(),
ImageShow.GmDisplayViewer(),
ImageShow.EogViewer(),
ImageShow.XVViewer(),
ImageShow.IPythonViewer(),
),
)
def test_show_file(viewer: ImageShow.Viewer) -> None:
assert not os.path.exists("missing.png")
with pytest.raises(FileNotFoundError):
viewer.show_file("missing.png")
def test_viewer() -> None: def test_viewer() -> None:
viewer = ImageShow.Viewer() viewer = ImageShow.Viewer()

View File

@ -25,10 +25,10 @@ def test_sanity() -> None:
st.stddev st.stddev
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
st.spam() st.spam() # type: ignore[attr-defined]
with pytest.raises(TypeError): with pytest.raises(TypeError):
ImageStat.Stat(1) ImageStat.Stat(1) # type: ignore[arg-type]
def test_hopper() -> None: def test_hopper() -> None:

View File

@ -45,10 +45,12 @@ def test_kw() -> None:
# Test "file" # Test "file"
im = ImageTk._get_image_from_kw(kw) im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im1) assert_image_equal(im, im1)
# Test "data" # Test "data"
im = ImageTk._get_image_from_kw(kw) im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im2) assert_image_equal(im, im2)
# Test no relevant entry # Test no relevant entry
@ -70,6 +72,11 @@ def test_photoimage(mode: str) -> None:
reloaded = ImageTk.getimage(im_tk) reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA")) assert_image_equal(reloaded, im.convert("RGBA"))
with pytest.raises(ValueError):
ImageTk.PhotoImage()
with pytest.raises(ValueError):
ImageTk.PhotoImage(mode)
def test_photoimage_apply_transparency() -> None: def test_photoimage_apply_transparency() -> None:
with Image.open("Tests/images/pil123p.png") as im: with Image.open("Tests/images/pil123p.png") as im:
@ -102,3 +109,6 @@ def test_bitmapimage() -> None:
# reloaded = ImageTk.getimage(im_tk) # reloaded = ImageTk.getimage(im_tk)
# assert_image_equal(reloaded, im) # assert_image_equal(reloaded, im)
with pytest.raises(ValueError):
ImageTk.BitmapImage()

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