Merge branch 'main' into init
|
@ -1,3 +1,10 @@
|
|||
skip_commits:
|
||||
files:
|
||||
- ".github/**/*"
|
||||
- ".gitmodules"
|
||||
- "docs/**/*"
|
||||
- "wheels/**/*"
|
||||
|
||||
version: '{build}'
|
||||
clone_folder: c:\pillow
|
||||
init:
|
||||
|
@ -6,6 +13,7 @@ init:
|
|||
# Uncomment previous line to get RDP access during the build.
|
||||
|
||||
environment:
|
||||
COVERAGE_CORE: sysmon
|
||||
EXECUTABLE: python.exe
|
||||
TEST_OPTIONS:
|
||||
DEPLOY: YES
|
||||
|
@ -26,7 +34,7 @@ install:
|
|||
- 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
|
||||
- 7z x nasm-win64.zip -oc:\
|
||||
- choco install ghostscript --version=10.0.0.20230317
|
||||
- choco install ghostscript --version=10.3.0
|
||||
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.16.5
|
||||
cibuildwheel==2.17.0
|
||||
|
|
1
.ci/requirements-mypy.txt
Normal file
|
@ -0,0 +1 @@
|
|||
mypy==1.9.0
|
|
@ -10,6 +10,11 @@ exclude_also =
|
|||
if DEBUG:
|
||||
# Don't complain about compatibility code for missing optional dependencies
|
||||
except ImportError
|
||||
if TYPE_CHECKING:
|
||||
@abc.abstractmethod
|
||||
# Empty bodies in protocols or abstract methods
|
||||
^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$
|
||||
^\s*\.\.\.(\s*#.*)?$
|
||||
|
||||
[run]
|
||||
omit =
|
||||
|
|
2
.github/FUNDING.yml
vendored
|
@ -1 +1 @@
|
|||
tidelift: "pypi/Pillow"
|
||||
tidelift: "pypi/pillow"
|
||||
|
|
15
.github/ISSUE_TEMPLATE/ISSUE_REPORT.md
vendored
|
@ -48,6 +48,21 @@ Thank you.
|
|||
* Python:
|
||||
* Pillow:
|
||||
|
||||
```text
|
||||
Please paste here the output of running:
|
||||
|
||||
python3 -m PIL.report
|
||||
or
|
||||
python3 -m PIL --report
|
||||
|
||||
Or the output of the following Python code:
|
||||
|
||||
from PIL import report
|
||||
# or
|
||||
from PIL import features
|
||||
features.pilinfo(supported_formats=False)
|
||||
```
|
||||
|
||||
<!--
|
||||
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.
|
||||
|
||||
|
|
2
.github/workflows/docs.yml
vendored
|
@ -7,10 +7,12 @@ on:
|
|||
paths:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
- "src/PIL/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
- "src/PIL/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
2
.github/workflows/release-drafter.yml
vendored
|
@ -23,6 +23,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Drafts your next release notes as pull requests are merged into "main"
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
1
.github/workflows/system-info.py
vendored
|
@ -6,6 +6,7 @@ This sort of info is missing from GitHub Actions.
|
|||
Requested here:
|
||||
https://github.com/actions/virtual-environments/issues/79
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
|
28
.github/workflows/test-cygwin.yml
vendored
|
@ -26,6 +26,9 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
@ -47,9 +50,8 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Cygwin
|
||||
uses: egor-tensin/setup-cygwin@v4
|
||||
uses: cygwin/cygwin-install-action@v4
|
||||
with:
|
||||
platform: x86_64
|
||||
packages: >
|
||||
gcc-g++
|
||||
ghostscript
|
||||
|
@ -69,7 +71,6 @@ jobs:
|
|||
make
|
||||
netpbm
|
||||
perl
|
||||
python39=3.9.16-1
|
||||
python3${{ matrix.python-minor-version }}-cffi
|
||||
python3${{ matrix.python-minor-version }}-cython
|
||||
python3${{ matrix.python-minor-version }}-devel
|
||||
|
@ -81,27 +82,21 @@ jobs:
|
|||
zlib-devel
|
||||
|
||||
- name: Add Lapack to PATH
|
||||
uses: egor-tensin/cleanup-path@v3
|
||||
uses: egor-tensin/cleanup-path@v4
|
||||
with:
|
||||
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
|
||||
|
||||
- name: Select Python version
|
||||
run: |
|
||||
ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/cygwin/bin/python3
|
||||
|
||||
- name: Get latest NumPy version
|
||||
id: latest-numpy
|
||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||
run: |
|
||||
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
|
||||
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
|
||||
|
||||
- name: pip cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
|
||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-
|
||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
|
||||
|
||||
- name: Build system information
|
||||
run: |
|
||||
|
@ -111,11 +106,6 @@ jobs:
|
|||
run: |
|
||||
bash.exe .ci/install.sh
|
||||
|
||||
- name: Upgrade NumPy
|
||||
shell: dash.exe -l "{0}"
|
||||
run: |
|
||||
python3 -m pip install -U "numpy<1.26"
|
||||
|
||||
- name: Build
|
||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||
run: |
|
||||
|
@ -142,7 +132,7 @@ jobs:
|
|||
bash.exe .ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Cygwin
|
||||
|
|
2
.github/workflows/test-docker.yml
vendored
|
@ -101,7 +101,7 @@ jobs:
|
|||
MATRIX_DOCKER: ${{ matrix.docker }}
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
flags: GHA_Docker
|
||||
name: ${{ matrix.docker }}
|
||||
|
|
7
.github/workflows/test-mingw.yml
vendored
|
@ -26,6 +26,9 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
@ -64,10 +67,10 @@ jobs:
|
|||
mingw-w64-x86_64-python3-cffi \
|
||||
mingw-w64-x86_64-python3-numpy \
|
||||
mingw-w64-x86_64-python3-olefile \
|
||||
mingw-w64-x86_64-python3-pip \
|
||||
mingw-w64-x86_64-python3-setuptools \
|
||||
mingw-w64-x86_64-python-pyqt6
|
||||
|
||||
python3 -m ensurepip
|
||||
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
|
||||
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
@ -82,7 +85,7 @@ jobs:
|
|||
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
|
|
19
.github/workflows/test-windows.yml
vendored
|
@ -26,6 +26,9 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
@ -66,8 +69,16 @@ jobs:
|
|||
- name: Print build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
||||
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
||||
- name: Install Python dependencies
|
||||
run: >
|
||||
python3 -m pip install
|
||||
coverage>=7.4.2
|
||||
defusedxml
|
||||
olefile
|
||||
pyroma
|
||||
pytest
|
||||
pytest-cov
|
||||
pytest-timeout
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
|
@ -75,7 +86,7 @@ jobs:
|
|||
choco install nasm --no-progress
|
||||
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||
|
||||
choco install ghostscript --version=10.0.0.20230317 --no-progress
|
||||
choco install ghostscript --version=10.3.0 --no-progress
|
||||
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
|
||||
|
||||
# Install extra test images
|
||||
|
@ -202,7 +213,7 @@ jobs:
|
|||
shell: pwsh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
|
|
3
.github/workflows/test.yml
vendored
|
@ -27,6 +27,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
|
@ -149,7 +150,7 @@ jobs:
|
|||
.ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
|
51
.github/workflows/wheels-dependencies.sh
vendored
|
@ -16,31 +16,31 @@ ARCHIVE_SDIR=pillow-depends-main
|
|||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.2
|
||||
HARFBUZZ_VERSION=8.3.0
|
||||
LIBPNG_VERSION=1.6.40
|
||||
JPEGTURBO_VERSION=3.0.1
|
||||
OPENJPEG_VERSION=2.5.0
|
||||
HARFBUZZ_VERSION=8.4.0
|
||||
LIBPNG_VERSION=1.6.43
|
||||
JPEGTURBO_VERSION=3.0.2
|
||||
OPENJPEG_VERSION=2.5.2
|
||||
XZ_VERSION=5.4.5
|
||||
TIFF_VERSION=4.6.0
|
||||
LCMS2_VERSION=2.16
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
GIFLIB_VERSION=5.1.4
|
||||
GIFLIB_VERSION=5.2.2
|
||||
else
|
||||
GIFLIB_VERSION=5.2.1
|
||||
fi
|
||||
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
|
||||
ZLIB_VERSION=1.3
|
||||
ZLIB_VERSION=1.3.1
|
||||
else
|
||||
ZLIB_VERSION=1.2.8
|
||||
fi
|
||||
LIBWEBP_VERSION=1.3.2
|
||||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.16
|
||||
LIBXCB_VERSION=1.16.1
|
||||
BROTLI_VERSION=1.1.0
|
||||
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
||||
function build_openjpeg {
|
||||
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz)
|
||||
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz)
|
||||
(cd $out_dir \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
||||
&& make install)
|
||||
|
@ -62,7 +62,7 @@ function build_brotli {
|
|||
|
||||
function build {
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
export BUILD_PREFIX="/usr/local"
|
||||
sudo chown -R runner /usr/local
|
||||
fi
|
||||
build_xz
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
|
||||
|
@ -72,13 +72,11 @@ function build {
|
|||
|
||||
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
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 libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
|
||||
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
||||
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||
if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
|
||||
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
|
||||
fi
|
||||
cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig
|
||||
fi
|
||||
else
|
||||
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
||||
|
@ -89,12 +87,10 @@ function build {
|
|||
build_tiff
|
||||
build_libpng
|
||||
build_lcms2
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do
|
||||
cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib
|
||||
done
|
||||
fi
|
||||
build_openjpeg
|
||||
if [ -f /usr/local/lib64/libopenjp2.so ]; then
|
||||
cp /usr/local/lib64/libopenjp2.so /usr/local/lib
|
||||
fi
|
||||
|
||||
ORIGINAL_CFLAGS=$CFLAGS
|
||||
CFLAGS="$CFLAGS -O3 -DNDEBUG"
|
||||
|
@ -130,14 +126,19 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de
|
|||
untar pillow-depends-main.zip
|
||||
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
|
||||
# libxdmcp causes an issue on macOS < 11
|
||||
# if php is installed, brew tries to reinstall these after installing openblas
|
||||
# libtiff and libxcb cause a conflict with building libtiff and libxcb
|
||||
# libxau and libxdmcp cause an issue on macOS < 11
|
||||
# remove cairo to fix building harfbuzz on arm64
|
||||
# remove lcms2 and libpng to fix building openjpeg on arm64
|
||||
# remove zstd to avoid inclusion on x86_64
|
||||
# remove jpeg-turbo to avoid inclusion on arm64
|
||||
# remove webp and zstd to avoid inclusion on x86_64
|
||||
# curl from brew requires zstd, use system curl
|
||||
brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd
|
||||
brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd
|
||||
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
brew remove --ignore-dependencies jpeg-turbo
|
||||
else
|
||||
brew remove --ignore-dependencies webp
|
||||
fi
|
||||
|
||||
brew install pkg-config
|
||||
fi
|
||||
|
|
3
.github/workflows/wheels-test.sh
vendored
|
@ -4,6 +4,9 @@ set -e
|
|||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
brew install fribidi
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then
|
||||
sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib
|
||||
fi
|
||||
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
|
||||
apk add curl fribidi
|
||||
else
|
||||
|
|
6
.github/workflows/wheels.yml
vendored
|
@ -5,6 +5,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
|
@ -14,6 +15,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
|
@ -99,7 +101,7 @@ jobs:
|
|||
cibw_arch: x86_64
|
||||
macosx_deployment_target: "10.10"
|
||||
- name: "macOS arm64"
|
||||
os: macos-latest
|
||||
os: macos-14
|
||||
cibw_arch: arm64
|
||||
macosx_deployment_target: "11.0"
|
||||
- name: "manylinux2014 and musllinux x86_64"
|
||||
|
@ -132,7 +134,7 @@ jobs:
|
|||
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_SKIP: pp38-*
|
||||
CIBW_TEST_SKIP: "*-macosx_arm64"
|
||||
CIBW_TEST_SKIP: cp38-macosx_arm64
|
||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.9
|
||||
rev: v0.3.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.12.1
|
||||
rev: 24.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.6
|
||||
rev: 1.7.8
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [--severity-level=high]
|
||||
files: ^src/
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.4
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
@ -42,18 +42,25 @@ repos:
|
|||
- id: trailing-whitespace
|
||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.28.1
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v0.9.1
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 1.5.3
|
||||
rev: 1.7.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.15
|
||||
rev: v0.16
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
|
@ -62,5 +69,10 @@ repos:
|
|||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
|
||||
- repo: meta
|
||||
hooks:
|
||||
- id: check-hooks-apply
|
||||
- id: check-useless-excludes
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
|
|
117
CHANGES.rst
|
@ -2,9 +2,120 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
10.3.0 (unreleased)
|
||||
10.3.0 (2024-04-01)
|
||||
-------------------
|
||||
|
||||
- CVE-2024-28219: Use ``strncpy`` to avoid buffer overflow #7928
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Deprecate ``eval()``, replacing it with ``lambda_eval()`` and ``unsafe_eval()`` #7927
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Raise ``ValueError`` if seeking to greater than offset-sized integer in TIFF #7883
|
||||
[radarhere]
|
||||
|
||||
- Add ``--report`` argument to ``__main__.py`` to omit supported formats #7818
|
||||
[nulano, radarhere, hugovk]
|
||||
|
||||
- Added RGB to I;16, I;16L, I;16B and I;16N conversion #7918, #7920
|
||||
[radarhere]
|
||||
|
||||
- Fix editable installation with custom build backend and configuration options #7658
|
||||
[nulano, radarhere]
|
||||
|
||||
- Fix putdata() for I;16N on big-endian #7209
|
||||
[Yay295, hugovk, radarhere]
|
||||
|
||||
- Determine MPO size from markers, not EXIF data #7884
|
||||
[radarhere]
|
||||
|
||||
- Improved conversion from RGB to RGBa, LA and La #7888
|
||||
[radarhere]
|
||||
|
||||
- Support FITS images with GZIP_1 compression #7894
|
||||
[radarhere]
|
||||
|
||||
- Use I;16 mode for 9-bit JPEG 2000 images #7900
|
||||
[scaramallion, radarhere]
|
||||
|
||||
- Raise ValueError if kmeans is negative #7891
|
||||
[radarhere]
|
||||
|
||||
- Remove TIFF tag OSUBFILETYPE when saving using libtiff #7893
|
||||
[radarhere]
|
||||
|
||||
- Raise ValueError for negative values when loading P1-P3 PPM images #7882
|
||||
[radarhere]
|
||||
|
||||
- Added reading of JPEG2000 palettes #7870
|
||||
[radarhere]
|
||||
|
||||
- Added alpha_quality argument when saving WebP images #7872
|
||||
[radarhere]
|
||||
|
||||
- Fixed joined corners for ImageDraw rounded_rectangle() non-integer dimensions #7881
|
||||
[radarhere]
|
||||
|
||||
- Stop reading EPS image at EOF marker #7753
|
||||
[radarhere]
|
||||
|
||||
- PSD layer co-ordinates may be negative #7706
|
||||
[radarhere]
|
||||
|
||||
- Use subprocess with CREATE_NO_WINDOW flag in ImageShow WindowsViewer #7791
|
||||
[radarhere]
|
||||
|
||||
- When saving GIF frame that restores to background color, do not fill identical pixels #7788
|
||||
[radarhere]
|
||||
|
||||
- Fixed reading PNG iCCP compression method #7823
|
||||
[radarhere]
|
||||
|
||||
- Allow writing IFDRational to UNDEFINED tag #7840
|
||||
[radarhere]
|
||||
|
||||
- Fix logged tag name when loading Exif data #7842
|
||||
[radarhere]
|
||||
|
||||
- Use maximum frame size in IHDR chunk when saving APNG images #7821
|
||||
[radarhere]
|
||||
|
||||
- Prevent opening P TGA images without a palette #7797
|
||||
[radarhere]
|
||||
|
||||
- Use palette when loading ICO images #7798
|
||||
[radarhere]
|
||||
|
||||
- Use consistent arguments for load_read and load_seek #7713
|
||||
[radarhere]
|
||||
|
||||
- Turn off nullability warnings for macOS SDK #7827
|
||||
[radarhere]
|
||||
|
||||
- Fix shift-sign issue in Convert.c #7838
|
||||
[r-barnes, radarhere]
|
||||
|
||||
- Open 16-bit grayscale PNGs as I;16 #7849
|
||||
[radarhere]
|
||||
|
||||
- Handle truncated chunks at the end of PNG images #7709
|
||||
[lajiyuan, radarhere]
|
||||
|
||||
- Match mask size to pasted image size in GifImagePlugin #7779
|
||||
[radarhere]
|
||||
|
||||
- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782
|
||||
[evanmiller, radarhere]
|
||||
|
||||
- Fixed reading FLI/FLC images with a prefix chunk #7804
|
||||
[twolife]
|
||||
|
||||
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
|
||||
[nik012003, radarhere]
|
||||
|
||||
- Remove execute bit from ``setup.py`` #7760
|
||||
[hugovk]
|
||||
|
||||
- Do not support using test-image-results to upload images after test failures #7739
|
||||
[radarhere]
|
||||
|
||||
|
@ -4235,7 +4346,7 @@ Changelog (Pillow)
|
|||
- Documentation changes, URL update, transpose, release checklist
|
||||
[radarhere]
|
||||
|
||||
- Fixed saving to nonexistant files specified by pathlib.Path objects #1748 (fixes #1747)
|
||||
- Fixed saving to nonexistent files specified by pathlib.Path objects #1748 (fixes #1747)
|
||||
[radarhere]
|
||||
|
||||
- Round Image.crop arguments to the nearest integer #1745 (fixes #1744)
|
||||
|
@ -7446,7 +7557,7 @@ The test suite includes 400 individual tests.
|
|||
- A handbook is available (distributed separately).
|
||||
|
||||
- The coordinate system is changed so that (0,0) is now located
|
||||
in the upper left corner. This is in compliancy with ISO 12087
|
||||
in the upper left corner. This is in compliance with ISO 12087
|
||||
and 90% of all other image processing and graphics libraries.
|
||||
|
||||
- Modes "1" (bilevel) and "P" (palette) have been introduced. Note
|
||||
|
|
4
LICENSE
|
@ -1,11 +1,11 @@
|
|||
The Python Imaging Library (PIL) is
|
||||
|
||||
Copyright © 1997-2011 by Secret Labs AB
|
||||
Copyright © 1995-2011 by Fredrik Lundh
|
||||
Copyright © 1995-2011 by Fredrik Lundh and contributors
|
||||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source HPND License:
|
||||
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
## Python Imaging Library (Fork)
|
||||
|
||||
Pillow is the friendly PIL fork by [Jeffrey A. Clark (Alex) and
|
||||
Pillow is the friendly PIL fork by [Jeffrey A. Clark and
|
||||
contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
|
||||
PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
|
||||
PIL is the Python Imaging Library by Fredrik Lundh and contributors.
|
||||
As of 2019, Pillow development is
|
||||
[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise).
|
||||
|
||||
|
@ -64,7 +64,7 @@ As of 2019, Pillow development is
|
|||
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a>
|
||||
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
|
||||
alt="Tidelift"
|
||||
src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a>
|
||||
src="https://tidelift.com/badges/package/pypi/pillow?style=flat"></a>
|
||||
<a href="https://pypi.org/project/pillow/"><img
|
||||
alt="Newest PyPI version"
|
||||
src="https://img.shields.io/pypi/v/pillow.svg"></a>
|
||||
|
@ -82,9 +82,6 @@ As of 2019, Pillow development is
|
|||
<a href="https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img
|
||||
alt="Join the chat at https://gitter.im/python-pillow/Pillow"
|
||||
src="https://badges.gitter.im/python-pillow/Pillow.svg"></a>
|
||||
<a href="https://twitter.com/PythonPillow"><img
|
||||
alt="Follow on https://twitter.com/PythonPillow"
|
||||
src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"></a>
|
||||
<a href="https://fosstodon.org/@pillow"><img
|
||||
alt="Follow on https://fosstodon.org/@pillow"
|
||||
src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg"
|
||||
|
|
|
@ -86,7 +86,7 @@ Released as needed privately to individual vendors for critical security-related
|
|||
|
||||
## Publicize Release
|
||||
|
||||
* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010
|
||||
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
|
@ -32,9 +32,8 @@ def timer(func, label, *args) -> None:
|
|||
break
|
||||
endtime = time.time()
|
||||
print(
|
||||
"{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format(
|
||||
label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0)
|
||||
)
|
||||
f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, "
|
||||
f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -23,7 +23,10 @@ def _get_mem_usage() -> float:
|
|||
|
||||
|
||||
def _test_leak(
|
||||
min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any
|
||||
min_iterations: int,
|
||||
max_iterations: int,
|
||||
fn: Callable[..., Image.Image | None],
|
||||
*args: Any,
|
||||
) -> None:
|
||||
mem_limit = None
|
||||
for i in range(max_iterations):
|
||||
|
|
|
@ -17,6 +17,7 @@ def test_ignore_dos_text() -> None:
|
|||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
for s in im.text.values():
|
||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||
|
||||
|
@ -32,6 +33,7 @@ def test_dos_text() -> None:
|
|||
assert msg, "Decompressed Data Too Large"
|
||||
return
|
||||
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
for s in im.text.values():
|
||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||
|
||||
|
@ -57,6 +59,7 @@ def test_dos_total_memory() -> None:
|
|||
return
|
||||
|
||||
total_len = 0
|
||||
assert isinstance(im2, PngImagePlugin.PngImageFile)
|
||||
for txt in im2.text.values():
|
||||
total_len += len(txt)
|
||||
assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Helper functions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
@ -10,6 +11,7 @@ import subprocess
|
|||
import sys
|
||||
import sysconfig
|
||||
import tempfile
|
||||
from functools import lru_cache
|
||||
from io import BytesIO
|
||||
from typing import Any, Callable, Sequence
|
||||
|
||||
|
@ -113,7 +115,9 @@ def assert_image_similar(
|
|||
|
||||
diff = 0
|
||||
for ach, bch in zip(a.split(), b.split()):
|
||||
chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L")
|
||||
chdiff = ImageMath.lambda_eval(
|
||||
lambda args: abs(args["a"] - args["b"]), a=ach, b=bch
|
||||
).convert("L")
|
||||
diff += sum(i * num for i, num in enumerate(chdiff.histogram()))
|
||||
|
||||
ave_diff = diff / (a.size[0] * a.size[1])
|
||||
|
@ -243,31 +247,33 @@ def fromstring(data: bytes) -> Image.Image:
|
|||
return Image.open(BytesIO(data))
|
||||
|
||||
|
||||
def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes:
|
||||
def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
|
||||
out = BytesIO()
|
||||
im.save(out, string_format, **options)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
|
||||
def hopper(mode: str | None = None) -> Image.Image:
|
||||
# Use caching to reduce reading from disk, but return a copy
|
||||
# so that the cached image isn't modified by the tests
|
||||
# (for fast, isolated, repeatable tests).
|
||||
|
||||
if mode is None:
|
||||
# Always return fresh not-yet-loaded version of image.
|
||||
# Operations on not-yet-loaded images is separate class of errors
|
||||
# what we should catch.
|
||||
# Operations on not-yet-loaded images are a separate class of errors
|
||||
# that we should catch.
|
||||
return Image.open("Tests/images/hopper.ppm")
|
||||
# Use caching to reduce reading from disk but so an original copy is
|
||||
# returned each time and the cached image isn't modified by tests
|
||||
# (for fast, isolated, repeatable tests).
|
||||
im = cache.get(mode)
|
||||
if im is None:
|
||||
if mode == "F":
|
||||
im = hopper("L").convert(mode)
|
||||
elif mode[:4] == "I;16":
|
||||
im = hopper("I").convert(mode)
|
||||
else:
|
||||
im = hopper().convert(mode)
|
||||
cache[mode] = im
|
||||
return im.copy()
|
||||
|
||||
return _cached_hopper(mode).copy()
|
||||
|
||||
|
||||
@lru_cache
|
||||
def _cached_hopper(mode: str) -> Image.Image:
|
||||
if mode == "F":
|
||||
im = hopper("L")
|
||||
else:
|
||||
im = hopper()
|
||||
return im.convert(mode)
|
||||
|
||||
|
||||
def djpeg_available() -> bool:
|
||||
|
@ -350,7 +356,7 @@ def is_mingw() -> bool:
|
|||
|
||||
|
||||
class CachedProperty:
|
||||
def __init__(self, func: Callable[[Any], None]) -> None:
|
||||
def __init__(self, func: Callable[[Any], Any]) -> None:
|
||||
self.func = func
|
||||
|
||||
def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
|
||||
|
|
BIN
Tests/icc/sGrey-v2-nano.icc
Normal file
Before Width: | Height: | Size: 578 B |
BIN
Tests/images/16_bit_binary_pgm.tiff
Normal file
BIN
Tests/images/2422.flc
Normal file
BIN
Tests/images/9bit.j2k
Normal file
Before Width: | Height: | Size: 298 KiB |
BIN
Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff
Normal file
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 180 B |
BIN
Tests/images/imagedraw_rectangle_I.tiff
Normal file
BIN
Tests/images/m13.fits
Normal file
366
Tests/images/m13_gzip.fits
Normal file
BIN
Tests/images/negative_top_left_layer.psd
Normal file
BIN
Tests/images/p_8.tga
Normal file
BIN
Tests/images/seek_too_large.tif
Normal file
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
BIN
Tests/images/truncated_end_chunk.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
Tests/images/unknown_compression_method.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
|
@ -7,7 +7,7 @@ import fuzzers
|
|||
import packaging
|
||||
import pytest
|
||||
|
||||
from PIL import Image, features
|
||||
from PIL import Image, UnidentifiedImageError, features
|
||||
from Tests.helper import skip_unless_feature
|
||||
|
||||
if sys.platform.startswith("win32"):
|
||||
|
@ -43,7 +43,7 @@ def test_fuzz_images(path: str) -> None:
|
|||
except (
|
||||
Image.DecompressionBombError,
|
||||
Image.DecompressionBombWarning,
|
||||
Image.UnidentifiedImageError,
|
||||
UnidentifiedImageError,
|
||||
):
|
||||
# Known Image.* exceptions
|
||||
assert True
|
||||
|
|
|
@ -10,7 +10,7 @@ from .helper import assert_image_similar
|
|||
base = os.path.join("Tests", "images", "bmp")
|
||||
|
||||
|
||||
def get_files(d, ext: str = ".bmp"):
|
||||
def get_files(d: str, ext: str = ".bmp") -> list[str]:
|
||||
return [
|
||||
os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f
|
||||
]
|
||||
|
@ -29,7 +29,7 @@ def test_bad() -> None:
|
|||
pass
|
||||
|
||||
|
||||
def test_questionable():
|
||||
def test_questionable() -> None:
|
||||
"""These shouldn't crash/dos, but it's not well defined that these
|
||||
are in spec"""
|
||||
supported = [
|
||||
|
@ -80,7 +80,7 @@ def test_good() -> None:
|
|||
"rgb32bf.bmp": "rgb24.png",
|
||||
}
|
||||
|
||||
def get_compare(f):
|
||||
def get_compare(f: str) -> str:
|
||||
name = os.path.split(f)[1]
|
||||
if name in file_map:
|
||||
return os.path.join(base, "html", file_map[name])
|
||||
|
|
|
@ -23,11 +23,11 @@ def test_imageops_box_blur() -> None:
|
|||
assert isinstance(i, Image.Image)
|
||||
|
||||
|
||||
def box_blur(image, radius: int = 1, n: int = 1):
|
||||
def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image:
|
||||
return image._new(image.im.box_blur((radius, radius), n))
|
||||
|
||||
|
||||
def assert_image(im, data, delta: int = 0) -> None:
|
||||
def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None:
|
||||
it = iter(im.getdata())
|
||||
for data_row in data:
|
||||
im_row = [next(it) for _ in range(im.size[0])]
|
||||
|
@ -37,7 +37,13 @@ def assert_image(im, data, delta: int = 0) -> None:
|
|||
next(it)
|
||||
|
||||
|
||||
def assert_blur(im, radius, data, passes: int = 1, delta: int = 0) -> None:
|
||||
def assert_blur(
|
||||
im: Image.Image,
|
||||
radius: float,
|
||||
data: list[list[int]],
|
||||
passes: int = 1,
|
||||
delta: int = 0,
|
||||
) -> None:
|
||||
# check grayscale image
|
||||
assert_image(box_blur(im, radius, passes), data, delta)
|
||||
rgba = Image.merge("RGBA", (im, im, im, im))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from array import array
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -8,6 +9,7 @@ from PIL import Image, ImageFilter
|
|||
|
||||
from .helper import assert_image_equal
|
||||
|
||||
numpy: ModuleType | None
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
|
@ -15,7 +17,9 @@ except ImportError:
|
|||
|
||||
|
||||
class TestColorLut3DCoreAPI:
|
||||
def generate_identity_table(self, channels, size):
|
||||
def generate_identity_table(
|
||||
self, channels: int, size: int | tuple[int, int, int]
|
||||
) -> tuple[int, int, int, int, list[float]]:
|
||||
if isinstance(size, tuple):
|
||||
size_1d, size_2d, size_3d = size
|
||||
else:
|
||||
|
@ -395,6 +399,7 @@ class TestColorLut3DFilter:
|
|||
|
||||
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
||||
def test_numpy_sources(self) -> None:
|
||||
assert numpy is not None
|
||||
table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16)
|
||||
with pytest.raises(ValueError, match="should have either channels"):
|
||||
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
|
||||
|
@ -428,6 +433,7 @@ class TestColorLut3DFilter:
|
|||
|
||||
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
||||
def test_numpy_formats(self) -> None:
|
||||
assert numpy is not None
|
||||
g = Image.linear_gradient("L")
|
||||
im = Image.merge(
|
||||
"RGB",
|
||||
|
|
|
@ -187,6 +187,6 @@ class TestEnvVars:
|
|||
{"PILLOW_BLOCKS_MAX": "wat"},
|
||||
),
|
||||
)
|
||||
def test_warnings(self, var) -> None:
|
||||
def test_warnings(self, var: dict[str, str]) -> None:
|
||||
with pytest.warns(UserWarning):
|
||||
Image._apply_env_variables(var)
|
||||
|
|
|
@ -20,7 +20,7 @@ from PIL import _deprecate
|
|||
),
|
||||
],
|
||||
)
|
||||
def test_version(version, expected) -> None:
|
||||
def test_version(version: int | None, expected: str) -> None:
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", version, "new thing")
|
||||
|
||||
|
@ -46,7 +46,7 @@ def test_unknown_version() -> None:
|
|||
),
|
||||
],
|
||||
)
|
||||
def test_old_version(deprecated, plural, expected) -> None:
|
||||
def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
|
||||
expected = r""
|
||||
with pytest.raises(RuntimeError, match=expected):
|
||||
_deprecate.deprecate(deprecated, 1, plural=plural)
|
||||
|
@ -76,7 +76,7 @@ def test_replacement_and_action() -> None:
|
|||
"Upgrade to new thing.",
|
||||
],
|
||||
)
|
||||
def test_action(action) -> None:
|
||||
def test_action(action: str) -> None:
|
||||
expected = (
|
||||
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
||||
r"Upgrade to new thing\."
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import io
|
||||
import re
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -29,7 +30,7 @@ def test_version() -> None:
|
|||
# Check the correctness of the convenience function
|
||||
# and the format of version numbers
|
||||
|
||||
def test(name, function) -> None:
|
||||
def test(name: str, function: Callable[[str], bool]) -> None:
|
||||
version = features.version(name)
|
||||
if not features.check(name):
|
||||
assert version is None
|
||||
|
@ -73,12 +74,12 @@ def test_libimagequant_version() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("feature", features.modules)
|
||||
def test_check_modules(feature) -> None:
|
||||
def test_check_modules(feature: str) -> None:
|
||||
assert features.check_module(feature) in [True, False]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("feature", features.codecs)
|
||||
def test_check_codecs(feature) -> None:
|
||||
def test_check_codecs(feature: str) -> None:
|
||||
assert features.check_codec(feature) in [True, False]
|
||||
|
||||
|
||||
|
@ -116,9 +117,10 @@ def test_unsupported_module() -> None:
|
|||
features.version_module(module)
|
||||
|
||||
|
||||
def test_pilinfo() -> None:
|
||||
@pytest.mark.parametrize("supported_formats", (True, False))
|
||||
def test_pilinfo(supported_formats) -> None:
|
||||
buf = io.StringIO()
|
||||
features.pilinfo(buf)
|
||||
features.pilinfo(buf, supported_formats=supported_formats)
|
||||
out = buf.getvalue()
|
||||
lines = out.splitlines()
|
||||
assert lines[0] == "-" * 68
|
||||
|
@ -128,9 +130,15 @@ def test_pilinfo() -> None:
|
|||
while lines[0].startswith(" "):
|
||||
lines = lines[1:]
|
||||
assert lines[0] == "-" * 68
|
||||
assert lines[1].startswith("Python modules loaded from ")
|
||||
assert lines[2].startswith("Binary modules loaded from ")
|
||||
assert lines[3] == "-" * 68
|
||||
assert lines[1].startswith("Python executable is")
|
||||
lines = lines[2:]
|
||||
if lines[0].startswith("Environment Python files loaded from"):
|
||||
lines = lines[1:]
|
||||
assert lines[0].startswith("System Python files loaded from")
|
||||
assert lines[1] == "-" * 68
|
||||
assert lines[2].startswith("Python Pillow modules loaded from ")
|
||||
assert lines[3].startswith("Binary Pillow modules loaded from ")
|
||||
assert lines[4] == "-" * 68
|
||||
jpeg = (
|
||||
"\n"
|
||||
+ "-" * 68
|
||||
|
@ -141,4 +149,4 @@ def test_pilinfo() -> None:
|
|||
+ "-" * 68
|
||||
+ "\n"
|
||||
)
|
||||
assert jpeg in out
|
||||
assert supported_formats == (jpeg in out)
|
||||
|
|
|
@ -47,7 +47,7 @@ def test_apng_basic() -> None:
|
|||
"filename",
|
||||
("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
|
||||
)
|
||||
def test_apng_fdat(filename) -> None:
|
||||
def test_apng_fdat(filename: str) -> None:
|
||||
with Image.open(filename) as im:
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
|
@ -338,7 +338,7 @@ def test_apng_syntax_errors() -> None:
|
|||
"sequence_fdat_fctl.png",
|
||||
),
|
||||
)
|
||||
def test_apng_sequence_errors(test_file) -> None:
|
||||
def test_apng_sequence_errors(test_file: str) -> None:
|
||||
with pytest.raises(SyntaxError):
|
||||
with Image.open(f"Tests/images/apng/{test_file}") as im:
|
||||
im.seek(im.n_frames - 1)
|
||||
|
@ -668,6 +668,16 @@ def test_apng_save_blend(tmp_path: Path) -> None:
|
|||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
|
||||
|
||||
def test_apng_save_size(tmp_path: Path) -> None:
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
im = Image.new("L", (100, 100))
|
||||
im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert reloaded.size == (200, 200)
|
||||
|
||||
|
||||
def test_seek_after_close() -> None:
|
||||
im = Image.open("Tests/images/apng/delay.png")
|
||||
im.seek(1)
|
||||
|
@ -681,7 +691,7 @@ def test_seek_after_close() -> None:
|
|||
@pytest.mark.parametrize("default_image", (True, False))
|
||||
@pytest.mark.parametrize("duplicate", (True, False))
|
||||
def test_different_modes_in_later_frames(
|
||||
mode, default_image, duplicate, tmp_path: Path
|
||||
mode: str, default_image: bool, duplicate: bool, tmp_path: Path
|
||||
) -> None:
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None:
|
|||
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
|
||||
],
|
||||
)
|
||||
def test_crashes(test_file) -> None:
|
||||
def test_crashes(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -16,7 +16,7 @@ from .helper import (
|
|||
|
||||
|
||||
def test_sanity(tmp_path: Path) -> None:
|
||||
def roundtrip(im) -> None:
|
||||
def roundtrip(im: Image.Image) -> None:
|
||||
outfile = str(tmp_path / "temp.bmp")
|
||||
|
||||
im.save(outfile, "BMP")
|
||||
|
@ -194,7 +194,7 @@ def test_rle4() -> None:
|
|||
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
||||
),
|
||||
)
|
||||
def test_rle8_eof(file_name, length) -> None:
|
||||
def test_rle8_eof(file_name: str, length: int) -> None:
|
||||
with open(file_name, "rb") as fp:
|
||||
data = fp.read(length)
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import ContainerIO, Image
|
||||
|
@ -21,9 +23,16 @@ def test_isatty() -> None:
|
|||
assert container.isatty() is False
|
||||
|
||||
|
||||
def test_seek_mode_0() -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"mode, expected_position",
|
||||
(
|
||||
(0, 33),
|
||||
(1, 66),
|
||||
(2, 100),
|
||||
),
|
||||
)
|
||||
def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None:
|
||||
# Arrange
|
||||
mode = 0
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
|
@ -32,39 +41,11 @@ def test_seek_mode_0() -> None:
|
|||
container.seek(33, mode)
|
||||
|
||||
# Assert
|
||||
assert container.tell() == 33
|
||||
|
||||
|
||||
def test_seek_mode_1() -> None:
|
||||
# Arrange
|
||||
mode = 1
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
# Act
|
||||
container.seek(33, mode)
|
||||
container.seek(33, mode)
|
||||
|
||||
# Assert
|
||||
assert container.tell() == 66
|
||||
|
||||
|
||||
def test_seek_mode_2() -> None:
|
||||
# Arrange
|
||||
mode = 2
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
# Act
|
||||
container.seek(33, mode)
|
||||
container.seek(33, mode)
|
||||
|
||||
# Assert
|
||||
assert container.tell() == 100
|
||||
assert container.tell() == expected_position
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_read_n0(bytesmode) -> None:
|
||||
def test_read_n0(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
@ -80,7 +61,7 @@ def test_read_n0(bytesmode) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_read_n(bytesmode) -> None:
|
||||
def test_read_n(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
@ -96,7 +77,7 @@ def test_read_n(bytesmode) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_read_eof(bytesmode) -> None:
|
||||
def test_read_eof(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
@ -112,7 +93,7 @@ def test_read_eof(bytesmode) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_readline(bytesmode) -> None:
|
||||
def test_readline(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||
|
@ -127,7 +108,7 @@ def test_readline(bytesmode) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_readlines(bytesmode) -> None:
|
||||
def test_readlines(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
expected = [
|
||||
"This is line 1\n",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Test DdsImagePlugin"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
@ -47,7 +48,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
|
|||
TEST_FILE_DX10_BC1_TYPELESS,
|
||||
),
|
||||
)
|
||||
def test_sanity_dxt1_bc1(image_path) -> None:
|
||||
def test_sanity_dxt1_bc1(image_path: str) -> None:
|
||||
"""Check DXT1 and BC1 images can be opened"""
|
||||
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
|
||||
target = target.convert("RGBA")
|
||||
|
@ -95,7 +96,7 @@ def test_sanity_dxt5() -> None:
|
|||
TEST_FILE_BC4U,
|
||||
),
|
||||
)
|
||||
def test_sanity_ati1_bc4u(image_path) -> None:
|
||||
def test_sanity_ati1_bc4u(image_path: str) -> None:
|
||||
"""Check ATI1 and BC4U images can be opened"""
|
||||
|
||||
with Image.open(image_path) as im:
|
||||
|
@ -116,7 +117,7 @@ def test_sanity_ati1_bc4u(image_path) -> None:
|
|||
TEST_FILE_DX10_BC4_TYPELESS,
|
||||
),
|
||||
)
|
||||
def test_dx10_bc4(image_path) -> None:
|
||||
def test_dx10_bc4(image_path: str) -> None:
|
||||
"""Check DX10 BC4 images can be opened"""
|
||||
|
||||
with Image.open(image_path) as im:
|
||||
|
@ -137,7 +138,7 @@ def test_dx10_bc4(image_path) -> None:
|
|||
TEST_FILE_BC5U,
|
||||
),
|
||||
)
|
||||
def test_sanity_ati2_bc5u(image_path) -> None:
|
||||
def test_sanity_ati2_bc5u(image_path: str) -> None:
|
||||
"""Check ATI2 and BC5U images can be opened"""
|
||||
|
||||
with Image.open(image_path) as im:
|
||||
|
@ -161,7 +162,7 @@ def test_sanity_ati2_bc5u(image_path) -> None:
|
|||
(TEST_FILE_BC5S, TEST_FILE_BC5S),
|
||||
),
|
||||
)
|
||||
def test_dx10_bc5(image_path, expected_path) -> None:
|
||||
def test_dx10_bc5(image_path: str, expected_path: str) -> None:
|
||||
"""Check DX10 BC5 images can be opened"""
|
||||
|
||||
with Image.open(image_path) as im:
|
||||
|
@ -175,7 +176,7 @@ def test_dx10_bc5(image_path, expected_path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS))
|
||||
def test_dx10_bc6h(image_path) -> None:
|
||||
def test_dx10_bc6h(image_path: str) -> None:
|
||||
"""Check DX10 BC6H/BC6HS images can be opened"""
|
||||
|
||||
with Image.open(image_path) as im:
|
||||
|
@ -256,7 +257,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None:
|
|||
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
|
||||
],
|
||||
)
|
||||
def test_uncompressed(mode, size, test_file) -> None:
|
||||
def test_uncompressed(mode: str, size: tuple[int, int], test_file: str) -> None:
|
||||
"""Check uncompressed images can be opened"""
|
||||
|
||||
with Image.open(test_file) as im:
|
||||
|
@ -358,7 +359,7 @@ def test_unsupported_bitcount() -> None:
|
|||
"Tests/images/unimplemented_pfflags.dds",
|
||||
),
|
||||
)
|
||||
def test_not_implemented(test_file) -> None:
|
||||
def test_not_implemented(test_file: str) -> None:
|
||||
with pytest.raises(NotImplementedError):
|
||||
with Image.open(test_file):
|
||||
pass
|
||||
|
@ -380,7 +381,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
|
|||
("RGBA", "Tests/images/pil123rgba.png"),
|
||||
],
|
||||
)
|
||||
def test_save(mode, test_file, tmp_path: Path) -> None:
|
||||
def test_save(mode: str, test_file: str, tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.dds")
|
||||
with Image.open(test_file) as im:
|
||||
assert im.mode == mode
|
||||
|
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import EpsImagePlugin, Image, features
|
||||
from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_similar,
|
||||
|
@ -84,7 +84,7 @@ simple_eps_file_with_long_binary_data = (
|
|||
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
|
||||
)
|
||||
@pytest.mark.parametrize("scale", (1, 2))
|
||||
def test_sanity(filename, size, scale) -> None:
|
||||
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
|
||||
expected_size = tuple(s * scale for s in size)
|
||||
with Image.open(filename) as image:
|
||||
image.load(scale=scale)
|
||||
|
@ -129,28 +129,28 @@ def test_binary_header_only() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_missing_version_comment(prefix) -> None:
|
||||
def test_missing_version_comment(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
|
||||
with pytest.raises(SyntaxError):
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_missing_boundingbox_comment(prefix) -> None:
|
||||
def test_missing_boundingbox_comment(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox))
|
||||
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_invalid_boundingbox_comment(prefix) -> None:
|
||||
def test_invalid_boundingbox_comment(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
|
||||
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None:
|
||||
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None:
|
||||
data = io.BytesIO(
|
||||
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
|
||||
)
|
||||
|
@ -161,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_ascii_comment_too_long(prefix) -> None:
|
||||
def test_ascii_comment_too_long(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
|
||||
with pytest.raises(SyntaxError, match="not an EPS file"):
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_long_binary_data(prefix) -> None:
|
||||
def test_long_binary_data(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
def test_load_long_binary_data(prefix) -> None:
|
||||
def test_load_long_binary_data(prefix: bytes) -> None:
|
||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
|
||||
with Image.open(data) as img:
|
||||
img.load()
|
||||
|
@ -305,7 +305,7 @@ def test_render_scale2() -> None:
|
|||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
|
||||
def test_resize(filename) -> None:
|
||||
def test_resize(filename: str) -> None:
|
||||
with Image.open(filename) as im:
|
||||
new_size = (100, 100)
|
||||
im = im.resize(new_size)
|
||||
|
@ -314,7 +314,7 @@ def test_resize(filename) -> None:
|
|||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
@pytest.mark.parametrize("filename", (FILE1, FILE2))
|
||||
def test_thumbnail(filename) -> None:
|
||||
def test_thumbnail(filename: str) -> None:
|
||||
# Issue #619
|
||||
with Image.open(filename) as im:
|
||||
new_size = (100, 100)
|
||||
|
@ -335,7 +335,7 @@ def test_readline_psfile(tmp_path: Path) -> None:
|
|||
line_endings = ["\r\n", "\n", "\n\r", "\r"]
|
||||
strings = ["something", "else", "baz", "bif"]
|
||||
|
||||
def _test_readline(t, ending) -> None:
|
||||
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
|
||||
ending = "Failure with line ending: %s" % (
|
||||
"".join("%s" % ord(s) for s in ending)
|
||||
)
|
||||
|
@ -344,13 +344,13 @@ def test_readline_psfile(tmp_path: Path) -> None:
|
|||
assert t.readline().strip("\r\n") == "baz", ending
|
||||
assert t.readline().strip("\r\n") == "bif", ending
|
||||
|
||||
def _test_readline_io_psfile(test_string, ending) -> None:
|
||||
def _test_readline_io_psfile(test_string: str, ending: str) -> None:
|
||||
f = io.BytesIO(test_string.encode("latin-1"))
|
||||
with pytest.warns(DeprecationWarning):
|
||||
t = EpsImagePlugin.PSFile(f)
|
||||
_test_readline(t, ending)
|
||||
|
||||
def _test_readline_file_psfile(test_string, ending) -> None:
|
||||
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"))
|
||||
|
@ -376,7 +376,7 @@ def test_psfile_deprecation() -> None:
|
|||
"line_ending",
|
||||
(b"\r\n", b"\n", b"\n\r", b"\r"),
|
||||
)
|
||||
def test_readline(prefix, line_ending) -> None:
|
||||
def test_readline(prefix: bytes, line_ending: bytes) -> None:
|
||||
simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
|
||||
data = io.BytesIO(simple_file)
|
||||
test_file = EpsImagePlugin.EpsImageFile(data)
|
||||
|
@ -394,7 +394,7 @@ def test_readline(prefix, line_ending) -> None:
|
|||
"Tests/images/illuCS6_preview.eps",
|
||||
),
|
||||
)
|
||||
def test_open_eps(filename) -> None:
|
||||
def test_open_eps(filename: str) -> None:
|
||||
# https://github.com/python-pillow/Pillow/issues/1104
|
||||
with Image.open(filename) as img:
|
||||
assert img.mode == "RGB"
|
||||
|
@ -417,9 +417,9 @@ def test_emptyline() -> None:
|
|||
"test_file",
|
||||
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
|
||||
)
|
||||
def test_timeout(test_file) -> None:
|
||||
def test_timeout(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with pytest.raises(Image.UnidentifiedImageError):
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open(f):
|
||||
pass
|
||||
|
||||
|
@ -437,3 +437,11 @@ def test_eof_before_bounding_box() -> None:
|
|||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
||||
pass
|
||||
|
||||
|
||||
def test_invalid_data_after_eof() -> None:
|
||||
with open("Tests/images/illuCS6_preview.eps", "rb") as f:
|
||||
img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255))
|
||||
|
||||
with Image.open(img_bytes) as img:
|
||||
assert img.mode == "RGB"
|
||||
|
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
|
||||
from PIL import FitsImagePlugin, Image
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
TEST_FILE = "Tests/images/hopper.fits"
|
||||
|
||||
|
@ -22,6 +22,11 @@ def test_open() -> None:
|
|||
assert_image_equal(im, hopper("L"))
|
||||
|
||||
|
||||
def test_gzip1() -> None:
|
||||
with Image.open("Tests/images/m13_gzip.fits") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/m13.fits")
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
# Arrange
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import FliImagePlugin, Image
|
||||
from PIL import FliImagePlugin, Image, ImageFile
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
||||
|
||||
|
@ -12,9 +12,12 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
|||
# save as...-> hopper.fli, default options.
|
||||
static_test_file = "Tests/images/hopper.fli"
|
||||
|
||||
# From https://samples.libav.org/fli-flc/
|
||||
# From https://samples.ffmpeg.org/fli-flc/
|
||||
animated_test_file = "Tests/images/a.fli"
|
||||
|
||||
# From https://samples.ffmpeg.org/fli-flc/
|
||||
animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
|
||||
|
||||
|
||||
def test_sanity() -> None:
|
||||
with Image.open(static_test_file) as im:
|
||||
|
@ -32,6 +35,24 @@ def test_sanity() -> None:
|
|||
assert im.is_animated
|
||||
|
||||
|
||||
def test_prefix_chunk() -> None:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
with Image.open(animated_test_file_with_prefix_chunk) as im:
|
||||
assert im.mode == "P"
|
||||
assert im.size == (320, 200)
|
||||
assert im.format == "FLI"
|
||||
assert im.info["duration"] == 171
|
||||
assert im.is_animated
|
||||
|
||||
palette = im.getpalette()
|
||||
assert palette[3:6] == [255, 255, 255]
|
||||
assert palette[381:384] == [204, 204, 12]
|
||||
assert palette[765:] == [252, 0, 0]
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
def open() -> None:
|
||||
|
@ -147,7 +168,7 @@ def test_seek() -> None:
|
|||
],
|
||||
)
|
||||
@pytest.mark.timeout(timeout=3)
|
||||
def test_timeouts(test_file) -> None:
|
||||
def test_timeouts(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
with pytest.raises(OSError):
|
||||
|
@ -160,7 +181,7 @@ def test_timeouts(test_file) -> None:
|
|||
"Tests/images/crash-5762152299364352.fli",
|
||||
],
|
||||
)
|
||||
def test_crash(test_file) -> None:
|
||||
def test_crash(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import warnings
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -144,13 +145,13 @@ def test_strategy() -> None:
|
|||
|
||||
|
||||
def test_optimize() -> None:
|
||||
def test_grayscale(optimize):
|
||||
def test_grayscale(optimize: int) -> int:
|
||||
im = Image.new("L", (1, 1), 0)
|
||||
filename = BytesIO()
|
||||
im.save(filename, "GIF", optimize=optimize)
|
||||
return len(filename.getvalue())
|
||||
|
||||
def test_bilevel(optimize):
|
||||
def test_bilevel(optimize: int) -> int:
|
||||
im = Image.new("1", (1, 1), 0)
|
||||
test_file = BytesIO()
|
||||
im.save(test_file, "GIF", optimize=optimize)
|
||||
|
@ -178,7 +179,9 @@ def test_optimize() -> None:
|
|||
(4, 513, 256),
|
||||
),
|
||||
)
|
||||
def test_optimize_correctness(colors, size, expected_palette_length) -> None:
|
||||
def test_optimize_correctness(
|
||||
colors: int, size: int, expected_palette_length: int
|
||||
) -> None:
|
||||
# 256 color Palette image, posterize to > 128 and < 128 levels.
|
||||
# Size bigger and smaller than 512x512.
|
||||
# Check the palette for number of colors allocated.
|
||||
|
@ -297,7 +300,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
|
|||
("Tests/images/dispose_bgnd_rgba.gif", "RGBA"),
|
||||
),
|
||||
)
|
||||
def test_loading_multiple_palettes(path, mode) -> None:
|
||||
def test_loading_multiple_palettes(path: str, mode: str) -> None:
|
||||
with Image.open(path) as im:
|
||||
assert im.mode == "P"
|
||||
first_frame_colors = im.palette.colors.keys()
|
||||
|
@ -347,9 +350,9 @@ def test_palette_handling(tmp_path: Path) -> None:
|
|||
def test_palette_434(tmp_path: Path) -> None:
|
||||
# see https://github.com/python-pillow/Pillow/issues/434
|
||||
|
||||
def roundtrip(im, *args, **kwargs):
|
||||
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im.copy().save(out, *args, **kwargs)
|
||||
im.copy().save(out, **kwargs)
|
||||
reloaded = Image.open(out)
|
||||
|
||||
return reloaded
|
||||
|
@ -429,7 +432,7 @@ def test_seek_rewind() -> None:
|
|||
("Tests/images/iss634.gif", 42),
|
||||
),
|
||||
)
|
||||
def test_n_frames(path, n_frames) -> None:
|
||||
def test_n_frames(path: str, n_frames: int) -> None:
|
||||
# Test is_animated before n_frames
|
||||
with Image.open(path) as im:
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
@ -541,7 +544,10 @@ def test_dispose_background_transparency() -> None:
|
|||
),
|
||||
),
|
||||
)
|
||||
def test_transparent_dispose(loading_strategy, expected_colors) -> None:
|
||||
def test_transparent_dispose(
|
||||
loading_strategy: GifImagePlugin.LoadingStrategy,
|
||||
expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],
|
||||
) -> None:
|
||||
GifImagePlugin.LOADING_STRATEGY = loading_strategy
|
||||
try:
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||
|
@ -641,6 +647,9 @@ def test_dispose2_palette(tmp_path: Path) -> None:
|
|||
# Center remains red every frame
|
||||
assert rgb_img.getpixel((50, 50)) == circle
|
||||
|
||||
# Check that frame transparency wasn't added unnecessarily
|
||||
assert img._frame_transparency is None
|
||||
|
||||
|
||||
def test_dispose2_diff(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
@ -728,6 +737,25 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
|
|||
assert im.n_frames == 3
|
||||
|
||||
|
||||
def test_dispose2_previous_frame(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im = Image.new("P", (100, 100))
|
||||
im.info["transparency"] = 0
|
||||
d = ImageDraw.Draw(im)
|
||||
d.rectangle([(0, 0), (100, 50)], 1)
|
||||
im.putpalette((0, 0, 0, 255, 0, 0))
|
||||
|
||||
im2 = Image.new("P", (100, 100))
|
||||
im2.putpalette((0, 0, 0))
|
||||
|
||||
im.save(out, save_all=True, append_images=[im2], disposal=[0, 2])
|
||||
|
||||
with Image.open(out) as im:
|
||||
im.seek(1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 0, 255)
|
||||
|
||||
|
||||
def test_transparency_in_second_frame(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/different_transparency.gif") as im:
|
||||
|
@ -889,7 +917,9 @@ def test_identical_frames(tmp_path: Path) -> None:
|
|||
1500,
|
||||
),
|
||||
)
|
||||
def test_identical_frames_to_single_frame(duration, tmp_path: Path) -> None:
|
||||
def test_identical_frames_to_single_frame(
|
||||
duration: int | list[int], tmp_path: Path
|
||||
) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im_list = [
|
||||
Image.new("L", (100, 100), "#000"),
|
||||
|
@ -1049,7 +1079,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
|
|||
def test_version(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
def assert_version_after_save(im, version) -> None:
|
||||
def assert_version_after_save(im: Image.Image, version: bytes) -> None:
|
||||
im.save(out)
|
||||
with Image.open(out) as reread:
|
||||
assert reread.info["version"] == version
|
||||
|
@ -1088,7 +1118,7 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
assert reread.n_frames == 3
|
||||
|
||||
# Tests appending using a generator
|
||||
def im_generator(ims):
|
||||
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||
yield from ims
|
||||
|
||||
im.save(out, save_all=True, append_images=im_generator(ims))
|
||||
|
@ -1105,6 +1135,21 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
assert reread.n_frames == 10
|
||||
|
||||
|
||||
def test_append_different_size_image(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im = Image.new("RGB", (100, 100))
|
||||
bigger_im = Image.new("RGB", (200, 200), "#f00")
|
||||
|
||||
im.save(out, save_all=True, append_images=[bigger_im])
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert reread.size == (100, 100)
|
||||
|
||||
reread.seek(1)
|
||||
assert reread.size == (100, 100)
|
||||
|
||||
|
||||
def test_transparent_optimize(tmp_path: Path) -> None:
|
||||
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
|
||||
# transparency.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -55,15 +56,15 @@ def test_handler(tmp_path: Path) -> None:
|
|||
loaded = False
|
||||
saved = False
|
||||
|
||||
def open(self, im) -> None:
|
||||
def open(self, im: Image.Image) -> None:
|
||||
self.opened = True
|
||||
|
||||
def load(self, im):
|
||||
def load(self, im: Image.Image) -> Image.Image:
|
||||
self.loaded = True
|
||||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def save(self, im, fp, filename) -> None:
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
handler = TestHandler()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -56,15 +57,15 @@ def test_handler(tmp_path: Path) -> None:
|
|||
loaded = False
|
||||
saved = False
|
||||
|
||||
def open(self, im) -> None:
|
||||
def open(self, im: Image.Image) -> None:
|
||||
self.opened = True
|
||||
|
||||
def load(self, im):
|
||||
def load(self, im: Image.Image) -> Image.Image:
|
||||
self.loaded = True
|
||||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def save(self, im, fp, filename) -> None:
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
handler = TestHandler()
|
||||
|
|
|
@ -38,6 +38,17 @@ def test_black_and_white() -> None:
|
|||
assert im.size == (16, 16)
|
||||
|
||||
|
||||
def test_palette(tmp_path: Path) -> None:
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
|
||||
im = Image.new("P", (16, 16))
|
||||
im.save(temp_file)
|
||||
|
||||
with Image.open(temp_file) as reloaded:
|
||||
assert reloaded.mode == "P"
|
||||
assert reloaded.palette is not None
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||
with pytest.raises(SyntaxError):
|
||||
|
@ -135,7 +146,7 @@ def test_different_bit_depths(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
||||
def test_save_to_bytes_bmp(mode) -> None:
|
||||
def test_save_to_bytes_bmp(mode: str) -> None:
|
||||
output = io.BytesIO()
|
||||
im = hopper(mode)
|
||||
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
|
||||
|
|
|
@ -82,7 +82,7 @@ def test_eoferror() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
|
||||
def test_roundtrip(mode, tmp_path: Path) -> None:
|
||||
def test_roundtrip(mode: str, tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.im")
|
||||
im = hopper(mode)
|
||||
im.save(out)
|
||||
|
|
|
@ -98,7 +98,7 @@ def test_i() -> None:
|
|||
assert ret == 97
|
||||
|
||||
|
||||
def test_dump(monkeypatch) -> None:
|
||||
def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
c = b"abc"
|
||||
# Temporarily redirect stdout
|
||||
|
|
|
@ -5,6 +5,8 @@ import re
|
|||
import warnings
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -32,6 +34,7 @@ from .helper import (
|
|||
skip_unless_feature,
|
||||
)
|
||||
|
||||
ElementTree: ModuleType | None
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
|
@ -42,16 +45,22 @@ TEST_FILE = "Tests/images/hopper.jpg"
|
|||
|
||||
@skip_unless_feature("jpg")
|
||||
class TestFileJpeg:
|
||||
def roundtrip(self, im, **options):
|
||||
def roundtrip_with_bytes(
|
||||
self, im: Image.Image, **options: Any
|
||||
) -> tuple[JpegImagePlugin.JpegImageFile, int]:
|
||||
out = BytesIO()
|
||||
im.save(out, "JPEG", **options)
|
||||
test_bytes = out.tell()
|
||||
out.seek(0)
|
||||
im = Image.open(out)
|
||||
im.bytes = test_bytes # for testing only
|
||||
return im
|
||||
reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out))
|
||||
return reloaded, test_bytes
|
||||
|
||||
def gen_random_image(self, size, mode: str = "RGB"):
|
||||
def roundtrip(
|
||||
self, im: Image.Image, **options: Any
|
||||
) -> JpegImagePlugin.JpegImageFile:
|
||||
return self.roundtrip_with_bytes(im, **options)[0]
|
||||
|
||||
def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image:
|
||||
"""Generates a very hard to compress file
|
||||
:param size: tuple
|
||||
:param mode: optional image mode
|
||||
|
@ -71,7 +80,7 @@ class TestFileJpeg:
|
|||
assert im.get_format_mimetype() == "image/jpeg"
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero(self, size, tmp_path: Path) -> None:
|
||||
def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None:
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
im = Image.new("RGB", size)
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -108,13 +117,11 @@ class TestFileJpeg:
|
|||
assert "comment" not in reloaded.info
|
||||
|
||||
# Test that a comment argument overrides the default comment
|
||||
for comment in ("Test comment text", b"Text comment text"):
|
||||
for comment in ("Test comment text", b"Test comment text"):
|
||||
out = BytesIO()
|
||||
im.save(out, format="JPEG", comment=comment)
|
||||
with Image.open(out) as reloaded:
|
||||
if not isinstance(comment, bytes):
|
||||
comment = comment.encode()
|
||||
assert reloaded.info["comment"] == comment
|
||||
assert reloaded.info["comment"] == b"Test comment text"
|
||||
|
||||
def test_cmyk(self) -> None:
|
||||
# Test CMYK handling. Thanks to Tim and Charlie for test data,
|
||||
|
@ -145,7 +152,7 @@ class TestFileJpeg:
|
|||
assert k > 0.9
|
||||
|
||||
def test_rgb(self) -> None:
|
||||
def getchannels(im):
|
||||
def getchannels(im: Image.Image) -> tuple[int, int, int]:
|
||||
return tuple(v[0] for v in im.layer)
|
||||
|
||||
im = hopper()
|
||||
|
@ -161,8 +168,8 @@ class TestFileJpeg:
|
|||
"test_image_path",
|
||||
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
|
||||
)
|
||||
def test_dpi(self, test_image_path) -> None:
|
||||
def test(xdpi, ydpi=None):
|
||||
def test_dpi(self, test_image_path: str) -> None:
|
||||
def test(xdpi: int, ydpi: int | None = None):
|
||||
with Image.open(test_image_path) as im:
|
||||
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
|
||||
return im.info.get("dpi")
|
||||
|
@ -207,7 +214,7 @@ class TestFileJpeg:
|
|||
ImageFile.MAXBLOCK * 4 + 3, # large block
|
||||
),
|
||||
)
|
||||
def test_icc_big(self, n) -> None:
|
||||
def test_icc_big(self, n: int) -> None:
|
||||
# Make sure that the "extra" support handles large blocks
|
||||
# The ICC APP marker can store 65519 bytes per marker, so
|
||||
# using a 4-byte test code should allow us to detect out of
|
||||
|
@ -245,13 +252,13 @@ class TestFileJpeg:
|
|||
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
|
||||
|
||||
def test_optimize(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
im2 = self.roundtrip(hopper(), optimize=0)
|
||||
im3 = self.roundtrip(hopper(), optimize=1)
|
||||
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||
im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0)
|
||||
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1)
|
||||
assert_image_equal(im1, im2)
|
||||
assert_image_equal(im1, im3)
|
||||
assert im1.bytes >= im2.bytes
|
||||
assert im1.bytes >= im3.bytes
|
||||
assert im1_bytes >= im2_bytes
|
||||
assert im1_bytes >= im3_bytes
|
||||
|
||||
def test_optimize_large_buffer(self, tmp_path: Path) -> None:
|
||||
# https://github.com/python-pillow/Pillow/issues/148
|
||||
|
@ -261,15 +268,15 @@ class TestFileJpeg:
|
|||
im.save(f, format="JPEG", optimize=True)
|
||||
|
||||
def test_progressive(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||
im2 = self.roundtrip(hopper(), progressive=False)
|
||||
im3 = self.roundtrip(hopper(), progressive=True)
|
||||
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), progressive=True)
|
||||
assert not im1.info.get("progressive")
|
||||
assert not im2.info.get("progressive")
|
||||
assert im3.info.get("progressive")
|
||||
|
||||
assert_image_equal(im1, im3)
|
||||
assert im1.bytes >= im3.bytes
|
||||
assert im1_bytes >= im3_bytes
|
||||
|
||||
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
|
@ -340,6 +347,7 @@ class TestFileJpeg:
|
|||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
transposed = ImageOps.exif_transpose(im)
|
||||
assert transposed is not None
|
||||
exif = transposed.getexif()
|
||||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
|
@ -418,14 +426,14 @@ class TestFileJpeg:
|
|||
assert im3.info.get("progression")
|
||||
|
||||
def test_quality(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
im2 = self.roundtrip(hopper(), quality=50)
|
||||
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||
im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50)
|
||||
assert_image(im1, im2.mode, im2.size)
|
||||
assert im1.bytes >= im2.bytes
|
||||
assert im1_bytes >= im2_bytes
|
||||
|
||||
im3 = self.roundtrip(hopper(), quality=0)
|
||||
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), quality=0)
|
||||
assert_image(im1, im3.mode, im3.size)
|
||||
assert im2.bytes > im3.bytes
|
||||
assert im2_bytes > im3_bytes
|
||||
|
||||
def test_smooth(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
|
@ -433,7 +441,7 @@ class TestFileJpeg:
|
|||
assert_image(im1, im2.mode, im2.size)
|
||||
|
||||
def test_subsampling(self) -> None:
|
||||
def getsampling(im):
|
||||
def getsampling(im: Image.Image):
|
||||
layer = im.layer
|
||||
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
|
||||
|
||||
|
@ -441,25 +449,25 @@ class TestFileJpeg:
|
|||
for subsampling in (-1, 3): # (default, invalid)
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||
for subsampling in (0, "4:4:4"):
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||
for subsampling1 in (0, "4:4:4"):
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling1)
|
||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||
for subsampling in (1, "4:2:2"):
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||
for subsampling1 in (1, "4:2:2"):
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling1)
|
||||
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
|
||||
for subsampling in (2, "4:2:0", "4:1:1"):
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||
for subsampling1 in (2, "4:2:0", "4:1:1"):
|
||||
im = self.roundtrip(hopper(), subsampling=subsampling1)
|
||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||
|
||||
# RGB colorspace
|
||||
for subsampling in (-1, 0, "4:4:4"):
|
||||
for subsampling1 in (-1, 0, "4:4:4"):
|
||||
# "4:4:4" doesn't really make sense for RGB, but the conversion
|
||||
# to an integer happens at a higher level
|
||||
im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
|
||||
im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1)
|
||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||
for subsampling in (1, "4:2:2", 2, "4:2:0", 3):
|
||||
for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3):
|
||||
with pytest.raises(OSError):
|
||||
self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
|
||||
self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
self.roundtrip(hopper(), subsampling="1:1:1")
|
||||
|
@ -530,7 +538,7 @@ class TestFileJpeg:
|
|||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
def test_qtables(self, tmp_path: Path) -> None:
|
||||
def _n_qtables_helper(n, test_file) -> None:
|
||||
def _n_qtables_helper(n: int, test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
im.save(f, qtables=[[n] * 64] * n)
|
||||
|
@ -666,7 +674,7 @@ class TestFileJpeg:
|
|||
"blocks, rows, markers",
|
||||
((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)),
|
||||
)
|
||||
def test_restart_markers(self, blocks, rows, markers) -> None:
|
||||
def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None:
|
||||
im = Image.new("RGB", (32, 32)) # 16 MCUs
|
||||
out = BytesIO()
|
||||
im.save(
|
||||
|
@ -724,13 +732,13 @@ class TestFileJpeg:
|
|||
assert im.format == "JPEG"
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr"))
|
||||
def test_save_correct_modes(self, mode) -> None:
|
||||
def test_save_correct_modes(self, mode: str) -> None:
|
||||
out = BytesIO()
|
||||
img = Image.new(mode, (20, 20))
|
||||
img.save(out, "JPEG")
|
||||
|
||||
@pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P"))
|
||||
def test_save_wrong_modes(self, mode) -> None:
|
||||
def test_save_wrong_modes(self, mode: str) -> None:
|
||||
# ref https://github.com/python-pillow/Pillow/issues/2005
|
||||
out = BytesIO()
|
||||
img = Image.new(mode, (20, 20))
|
||||
|
@ -817,7 +825,7 @@ class TestFileJpeg:
|
|||
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
|
||||
# Act / Assert
|
||||
# "When the image resolution is unknown, 72 [dpi] is designated."
|
||||
# https://exiv2.org/tags.html
|
||||
# https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_invalid_exif(self) -> None:
|
||||
|
@ -982,16 +990,10 @@ class TestFileJpeg:
|
|||
# Even though this decoder never says that it is finished
|
||||
# the image should still end when there is no new data
|
||||
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
|
||||
def decode(self, buffer):
|
||||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
return 0, 0
|
||||
|
||||
decoder = InfiniteMockPyDecoder(None)
|
||||
|
||||
def closure(mode, *args):
|
||||
decoder.__init__(mode, *args)
|
||||
return decoder
|
||||
|
||||
Image.register_decoder("INFINITE", closure)
|
||||
Image.register_decoder("INFINITE", InfiniteMockPyDecoder)
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.tile = [
|
||||
|
|
|
@ -4,6 +4,7 @@ import os
|
|||
import re
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -36,13 +37,11 @@ test_card.load()
|
|||
# 'Not enough memory to handle tile data'
|
||||
|
||||
|
||||
def roundtrip(im, **options):
|
||||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
||||
out = BytesIO()
|
||||
im.save(out, "JPEG2000", **options)
|
||||
test_bytes = out.tell()
|
||||
out.seek(0)
|
||||
with Image.open(out) as im:
|
||||
im.bytes = test_bytes # for testing only
|
||||
im.load()
|
||||
return im
|
||||
|
||||
|
@ -76,7 +75,9 @@ def test_invalid_file() -> None:
|
|||
def test_bytesio() -> None:
|
||||
with open("Tests/images/test-card-lossless.jp2", "rb") as f:
|
||||
data = BytesIO(f.read())
|
||||
assert_image_similar_tofile(test_card, data, 1.0e-3)
|
||||
with Image.open(data) as im:
|
||||
im.load()
|
||||
assert_image_similar(im, test_card, 1.0e-3)
|
||||
|
||||
|
||||
# These two test pre-written JPEG 2000 files that were not written with
|
||||
|
@ -138,7 +139,7 @@ def test_prog_res_rt() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("num_resolutions", range(2, 6))
|
||||
def test_default_num_resolutions(num_resolutions) -> None:
|
||||
def test_default_num_resolutions(num_resolutions: int) -> None:
|
||||
d = 1 << (num_resolutions - 1)
|
||||
im = test_card.resize((d - 1, d - 1))
|
||||
with pytest.raises(OSError):
|
||||
|
@ -198,9 +199,9 @@ def test_layers_type(tmp_path: Path) -> None:
|
|||
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
|
||||
test_card.save(outfile, quality_layers=quality_layers)
|
||||
|
||||
for quality_layers in ["quality_layers", ("100", "50", "10")]:
|
||||
for quality_layers_str in ["quality_layers", ("100", "50", "10")]:
|
||||
with pytest.raises(ValueError):
|
||||
test_card.save(outfile, quality_layers=quality_layers)
|
||||
test_card.save(outfile, quality_layers=quality_layers_str)
|
||||
|
||||
|
||||
def test_layers() -> None:
|
||||
|
@ -233,7 +234,7 @@ def test_layers() -> None:
|
|||
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
|
||||
),
|
||||
)
|
||||
def test_no_jp2(name, args, offset, data) -> None:
|
||||
def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None:
|
||||
out = BytesIO()
|
||||
if name:
|
||||
out.name = name
|
||||
|
@ -278,7 +279,7 @@ def test_sgnd(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||
def test_rgba(ext) -> None:
|
||||
def test_rgba(ext: str) -> None:
|
||||
# Arrange
|
||||
with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
|
||||
# Act
|
||||
|
@ -289,7 +290,7 @@ def test_rgba(ext) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||
def test_16bit_monochrome_has_correct_mode(ext) -> None:
|
||||
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
|
||||
with Image.open("Tests/images/16bit.cropped" + ext) as im:
|
||||
im.load()
|
||||
assert im.mode == "I;16"
|
||||
|
@ -339,6 +340,7 @@ def test_parser_feed() -> None:
|
|||
p.feed(data)
|
||||
|
||||
# Assert
|
||||
assert p.image is not None
|
||||
assert p.image.size == (640, 480)
|
||||
|
||||
|
||||
|
@ -346,12 +348,12 @@ def test_parser_feed() -> None:
|
|||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2"))
|
||||
def test_subsampling_decode(name) -> None:
|
||||
def test_subsampling_decode(name: str) -> None:
|
||||
test = f"{EXTRA_DIR}/{name}.jp2"
|
||||
reference = f"{EXTRA_DIR}/{name}.ppm"
|
||||
|
||||
with Image.open(test) as im:
|
||||
epsilon = 3 # for YCbCr images
|
||||
epsilon = 3.0 # for YCbCr images
|
||||
with Image.open(reference) as im2:
|
||||
width, height = im2.size
|
||||
if name[-1] == "2":
|
||||
|
@ -362,6 +364,16 @@ def test_subsampling_decode(name) -> None:
|
|||
assert_image_similar(im, expected, epsilon)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
def test_pclr() -> None:
|
||||
with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im:
|
||||
assert im.mode == "P"
|
||||
assert len(im.palette.colors) == 256
|
||||
assert im.palette.colors[(255, 255, 255)] == 0
|
||||
|
||||
|
||||
def test_comment() -> None:
|
||||
with Image.open("Tests/images/comment.jp2") as im:
|
||||
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
|
||||
|
@ -400,7 +412,7 @@ def test_save_comment() -> None:
|
|||
"Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k",
|
||||
],
|
||||
)
|
||||
def test_crashes(test_file) -> None:
|
||||
def test_crashes(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
# Valgrind should not complain here
|
||||
|
@ -434,3 +446,9 @@ def test_plt_marker() -> None:
|
|||
hdr = out.read(2)
|
||||
length = _binary.i16be(hdr)
|
||||
out.seek(length - 2, os.SEEK_CUR)
|
||||
|
||||
|
||||
def test_9bit():
|
||||
with Image.open("Tests/images/9bit.j2k") as im:
|
||||
assert im.mode == "I;16"
|
||||
assert im.size == (128, 128)
|
||||
|
|
|
@ -6,13 +6,13 @@ import itertools
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
|
||||
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||
from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -27,7 +27,7 @@ from .helper import (
|
|||
|
||||
@skip_unless_feature("libtiff")
|
||||
class LibTiffTestCase:
|
||||
def _assert_noerr(self, tmp_path: Path, im) -> None:
|
||||
def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None:
|
||||
"""Helper tests that assert basic sanity about the g4 tiff reading"""
|
||||
# 1 bit
|
||||
assert im.mode == "1"
|
||||
|
@ -140,7 +140,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
||||
|
||||
@pytest.mark.parametrize("legacy_api", (False, True))
|
||||
def test_write_metadata(self, legacy_api, tmp_path: Path) -> None:
|
||||
def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None:
|
||||
"""Test metadata writing through libtiff"""
|
||||
f = str(tmp_path / "temp.tiff")
|
||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||
|
@ -243,36 +243,40 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
def test_custom_metadata(self, tmp_path: Path) -> None:
|
||||
tc = namedtuple("test_case", "value,type,supported_by_default")
|
||||
class Tc(NamedTuple):
|
||||
value: Any
|
||||
type: int
|
||||
supported_by_default: bool
|
||||
|
||||
custom = {
|
||||
37000 + k: v
|
||||
for k, v in enumerate(
|
||||
[
|
||||
tc(4, TiffTags.SHORT, True),
|
||||
tc(123456789, TiffTags.LONG, True),
|
||||
tc(-4, TiffTags.SIGNED_BYTE, False),
|
||||
tc(-4, TiffTags.SIGNED_SHORT, False),
|
||||
tc(-123456789, TiffTags.SIGNED_LONG, False),
|
||||
tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
|
||||
tc(4.25, TiffTags.FLOAT, True),
|
||||
tc(4.25, TiffTags.DOUBLE, True),
|
||||
tc("custom tag value", TiffTags.ASCII, True),
|
||||
tc(b"custom tag value", TiffTags.BYTE, True),
|
||||
tc((4, 5, 6), TiffTags.SHORT, True),
|
||||
tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True),
|
||||
tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
|
||||
tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
|
||||
tc(
|
||||
Tc(4, TiffTags.SHORT, True),
|
||||
Tc(123456789, TiffTags.LONG, True),
|
||||
Tc(-4, TiffTags.SIGNED_BYTE, False),
|
||||
Tc(-4, TiffTags.SIGNED_SHORT, False),
|
||||
Tc(-123456789, TiffTags.SIGNED_LONG, False),
|
||||
Tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
|
||||
Tc(4.25, TiffTags.FLOAT, True),
|
||||
Tc(4.25, TiffTags.DOUBLE, True),
|
||||
Tc("custom tag value", TiffTags.ASCII, True),
|
||||
Tc(b"custom tag value", TiffTags.BYTE, True),
|
||||
Tc((4, 5, 6), TiffTags.SHORT, True),
|
||||
Tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True),
|
||||
Tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
|
||||
Tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
|
||||
Tc(
|
||||
(-123456789, 9, 34, 234, 219387, -92432323),
|
||||
TiffTags.SIGNED_LONG,
|
||||
False,
|
||||
),
|
||||
tc((4.25, 5.25), TiffTags.FLOAT, True),
|
||||
tc((4.25, 5.25), TiffTags.DOUBLE, True),
|
||||
Tc((4.25, 5.25), TiffTags.FLOAT, True),
|
||||
Tc((4.25, 5.25), TiffTags.DOUBLE, True),
|
||||
# array of TIFF_BYTE requires bytes instead of tuple for backwards
|
||||
# compatibility
|
||||
tc(bytes([4]), TiffTags.BYTE, True),
|
||||
tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
|
||||
Tc(bytes([4]), TiffTags.BYTE, True),
|
||||
Tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
@ -284,7 +288,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
for libtiff in libtiffs:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
||||
|
||||
def check_tags(tiffinfo) -> None:
|
||||
def check_tags(
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
||||
) -> None:
|
||||
im = hopper()
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
@ -323,6 +329,12 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
)
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
def test_osubfiletype(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
||||
im.tag_v2[OSUBFILETYPE] = 1
|
||||
im.save(outfile)
|
||||
|
||||
def test_subifd(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
||||
|
@ -502,7 +514,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert_image_equal_tofile(im, out)
|
||||
|
||||
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
|
||||
def test_palette_save(self, im, tmp_path: Path) -> None:
|
||||
def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
|
@ -514,7 +526,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert len(reloaded.tag_v2[320]) == 768
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
|
||||
def test_bw_compression_w_rgb(self, compression, tmp_path: Path) -> None:
|
||||
def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None:
|
||||
im = hopper("RGB")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
|
@ -522,7 +534,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.save(out, compression=compression)
|
||||
|
||||
def test_fp_leak(self) -> None:
|
||||
im = Image.open("Tests/images/hopper_g4_500.tif")
|
||||
im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif")
|
||||
assert im is not None
|
||||
fn = im.fp.fileno()
|
||||
|
||||
os.fstat(fn)
|
||||
|
@ -647,7 +660,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Generate test image
|
||||
pilim = hopper()
|
||||
|
||||
def save_bytesio(compression=None) -> None:
|
||||
def save_bytesio(compression: str | None = None) -> None:
|
||||
buffer_io = io.BytesIO()
|
||||
pilim.save(buffer_io, format="tiff", compression=compression)
|
||||
buffer_io.seek(0)
|
||||
|
@ -714,6 +727,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
f.write(src.read())
|
||||
|
||||
im = Image.open(tmpfile)
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.n_frames
|
||||
im.close()
|
||||
# Should not raise PermissionError.
|
||||
|
@ -731,7 +745,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert icc == icc_libtiff
|
||||
|
||||
def test_write_icc(self, tmp_path: Path) -> None:
|
||||
def check_write(libtiff) -> None:
|
||||
def check_write(libtiff: bool) -> None:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
||||
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
|
@ -837,7 +851,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert reloaded.mode == "F"
|
||||
assert reloaded.getexif()[SAMPLEFORMAT] == 3
|
||||
|
||||
def test_lzma(self, capfd):
|
||||
def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
try:
|
||||
with Image.open("Tests/images/hopper_lzma.tif") as im:
|
||||
assert im.mode == "RGB"
|
||||
|
@ -853,7 +867,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
sys.stderr.write(captured.err)
|
||||
raise
|
||||
|
||||
def test_webp(self, capfd):
|
||||
def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
try:
|
||||
with Image.open("Tests/images/hopper_webp.tif") as im:
|
||||
assert im.mode == "RGB"
|
||||
|
@ -971,7 +985,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
|
||||
|
||||
@pytest.mark.parametrize("compression", (None, "jpeg"))
|
||||
def test_block_tile_tags(self, compression, tmp_path: Path) -> None:
|
||||
def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
|
@ -1020,7 +1034,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
),
|
||||
],
|
||||
)
|
||||
def test_wrong_bits_per_sample(self, file_name, mode, size, tile) -> None:
|
||||
def test_wrong_bits_per_sample(
|
||||
self, file_name: str, mode: str, size: tuple[int, int], tile
|
||||
) -> None:
|
||||
with Image.open("Tests/images/" + file_name) as im:
|
||||
assert im.mode == mode
|
||||
assert im.size == size
|
||||
|
@ -1086,35 +1102,37 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||
def test_save_multistrip(self, compression, tmp_path: Path) -> None:
|
||||
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
||||
im = hopper("RGB").resize((256, 256))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
im.save(out, compression=compression)
|
||||
|
||||
with Image.open(out) as im:
|
||||
# Assert that there are multiple strips
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[STRIPOFFSETS]) > 1
|
||||
|
||||
@pytest.mark.parametrize("argument", (True, False))
|
||||
def test_save_single_strip(self, argument, tmp_path: Path) -> None:
|
||||
def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None:
|
||||
im = hopper("RGB").resize((256, 256))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
if not argument:
|
||||
TiffImagePlugin.STRIP_SIZE = 2**18
|
||||
try:
|
||||
arguments = {"compression": "tiff_adobe_deflate"}
|
||||
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
|
||||
if argument:
|
||||
arguments["strip_size"] = 2**18
|
||||
im.save(out, **arguments)
|
||||
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[STRIPOFFSETS]) == 1
|
||||
finally:
|
||||
TiffImagePlugin.STRIP_SIZE = 65536
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
|
||||
def test_save_zero(self, compression, tmp_path: Path) -> None:
|
||||
def test_save_zero(self, compression: str | None, tmp_path: Path) -> None:
|
||||
im = Image.new("RGB", (0, 0))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
with pytest.raises(SystemError):
|
||||
|
@ -1134,7 +1152,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
("Tests/images/child_ifd_jpeg.tiff", (20,)),
|
||||
),
|
||||
)
|
||||
def test_get_child_images(self, path, sizes) -> None:
|
||||
def test_get_child_images(self, path: str, sizes: tuple[int, ...]) -> None:
|
||||
with Image.open(path) as im:
|
||||
ims = im.get_child_images()
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ from .test_file_libtiff import LibTiffTestCase
|
|||
|
||||
|
||||
class TestFileLibTiffSmall(LibTiffTestCase):
|
||||
|
||||
"""The small lena image was failing on open in the libtiff
|
||||
decoder because the file pointer was set to the wrong place
|
||||
by a spurious seek. It wasn't failing with the byteio method.
|
||||
|
|
|
@ -19,7 +19,7 @@ def test_valid_file() -> None:
|
|||
# https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8
|
||||
# https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/
|
||||
test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara"
|
||||
saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png"
|
||||
saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff"
|
||||
|
||||
# Act
|
||||
with Image.open(test_file) as im:
|
||||
|
|
|
@ -2,10 +2,11 @@ from __future__ import annotations
|
|||
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, MpoImagePlugin
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -19,18 +20,15 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
|
|||
pytestmark = skip_unless_feature("jpg")
|
||||
|
||||
|
||||
def roundtrip(im, **options):
|
||||
def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile:
|
||||
out = BytesIO()
|
||||
im.save(out, "MPO", **options)
|
||||
test_bytes = out.tell()
|
||||
out.seek(0)
|
||||
im = Image.open(out)
|
||||
im.bytes = test_bytes # for testing only
|
||||
return im
|
||||
return cast(MpoImagePlugin.MpoImageFile, Image.open(out))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_sanity(test_file) -> None:
|
||||
def test_sanity(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
im.load()
|
||||
assert im.mode == "RGB"
|
||||
|
@ -70,7 +68,7 @@ def test_context_manager() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_app(test_file) -> None:
|
||||
def test_app(test_file: str) -> None:
|
||||
# Test APP/COM reader (@PIL135)
|
||||
with Image.open(test_file) as im:
|
||||
assert im.applist[0][0] == "APP1"
|
||||
|
@ -82,7 +80,7 @@ def test_app(test_file) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_exif(test_file) -> None:
|
||||
def test_exif(test_file: str) -> None:
|
||||
with Image.open(test_file) as im_original:
|
||||
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
|
||||
|
||||
|
@ -95,7 +93,7 @@ def test_exif(test_file) -> None:
|
|||
|
||||
def test_frame_size() -> None:
|
||||
# This image has been hexedited to contain a different size
|
||||
# in the EXIF data of the second frame
|
||||
# in the SOF marker of the second frame
|
||||
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
|
||||
assert im.size == (640, 480)
|
||||
|
||||
|
@ -143,7 +141,7 @@ def test_reload_exif_after_seek() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_mp(test_file) -> None:
|
||||
def test_mp(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
mpinfo = im._getmp()
|
||||
assert mpinfo[45056] == b"0100"
|
||||
|
@ -168,7 +166,7 @@ def test_mp_no_data() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_mp_attribute(test_file) -> None:
|
||||
def test_mp_attribute(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
mpinfo = im._getmp()
|
||||
for frame_number, mpentry in enumerate(mpinfo[0xB002]):
|
||||
|
@ -185,7 +183,7 @@ def test_mp_attribute(test_file) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_seek(test_file) -> None:
|
||||
def test_seek(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
assert im.tell() == 0
|
||||
# prior to first image raises an error, both blatant and borderline
|
||||
|
@ -229,7 +227,7 @@ def test_eoferror() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_image_grab(test_file) -> None:
|
||||
def test_image_grab(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
assert im.tell() == 0
|
||||
im0 = im.tobytes()
|
||||
|
@ -244,7 +242,7 @@ def test_image_grab(test_file) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_save(test_file) -> None:
|
||||
def test_save(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
assert im.tell() == 0
|
||||
jpg0 = roundtrip(im)
|
||||
|
|
|
@ -52,7 +52,7 @@ def test_open_windows_v1() -> None:
|
|||
assert isinstance(im, MspImagePlugin.MspImageFile)
|
||||
|
||||
|
||||
def _assert_file_image_equal(source_path, target_path) -> None:
|
||||
def _assert_file_image_equal(source_path: str, target_path: str) -> None:
|
||||
with Image.open(source_path) as im:
|
||||
assert_image_equal_tofile(im, target_path)
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from PIL import Image
|
|||
from .helper import assert_image_equal, hopper, magick_command
|
||||
|
||||
|
||||
def helper_save_as_palm(tmp_path: Path, mode) -> None:
|
||||
def helper_save_as_palm(tmp_path: Path, mode: str) -> None:
|
||||
# Arrange
|
||||
im = hopper(mode)
|
||||
outfile = str(tmp_path / ("temp_" + mode + ".palm"))
|
||||
|
@ -24,7 +24,7 @@ def helper_save_as_palm(tmp_path: Path, mode) -> None:
|
|||
assert os.path.getsize(outfile) > 0
|
||||
|
||||
|
||||
def open_with_magick(magick, tmp_path: Path, f):
|
||||
def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image:
|
||||
outfile = str(tmp_path / "temp.png")
|
||||
rc = subprocess.call(
|
||||
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
|
||||
|
@ -33,7 +33,7 @@ def open_with_magick(magick, tmp_path: Path, f):
|
|||
return Image.open(outfile)
|
||||
|
||||
|
||||
def roundtrip(tmp_path: Path, mode) -> None:
|
||||
def roundtrip(tmp_path: Path, mode: str) -> None:
|
||||
magick = magick_command()
|
||||
if not magick:
|
||||
return
|
||||
|
@ -66,6 +66,6 @@ def test_p_mode(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "RGB"))
|
||||
def test_oserror(tmp_path: Path, mode) -> None:
|
||||
def test_oserror(tmp_path: Path, mode: str) -> None:
|
||||
with pytest.raises(OSError):
|
||||
helper_save_as_palm(tmp_path, mode)
|
||||
|
|
|
@ -9,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin
|
|||
from .helper import assert_image_equal, hopper
|
||||
|
||||
|
||||
def _roundtrip(tmp_path: Path, im) -> None:
|
||||
def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
|
||||
f = str(tmp_path / "temp.pcx")
|
||||
im.save(f)
|
||||
with Image.open(f) as im2:
|
||||
|
@ -44,7 +44,7 @@ def test_invalid_file() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
|
||||
def test_odd(tmp_path: Path, mode) -> None:
|
||||
def test_odd(tmp_path: Path, mode: str) -> None:
|
||||
# See issue #523, odd sized images should have a stride that's even.
|
||||
# Not that ImageMagick or GIMP write PCX that way.
|
||||
# We were not handling properly.
|
||||
|
@ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None:
|
|||
_roundtrip(tmp_path, im)
|
||||
|
||||
|
||||
def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None:
|
||||
def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None:
|
||||
_last = ImageFile.MAXBLOCK
|
||||
ImageFile.MAXBLOCK = size
|
||||
try:
|
||||
|
|
|
@ -6,6 +6,7 @@ import os.path
|
|||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Generator
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -14,7 +15,7 @@ from PIL import Image, PdfParser, features
|
|||
from .helper import hopper, mark_if_feature_version, skip_unless_feature
|
||||
|
||||
|
||||
def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
|
||||
def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
|
||||
# Arrange
|
||||
im = hopper(mode)
|
||||
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
|
||||
|
@ -41,13 +42,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
|
||||
def test_save(tmp_path: Path, mode) -> None:
|
||||
def test_save(tmp_path: Path, mode: str) -> None:
|
||||
helper_save_as_pdf(tmp_path, mode)
|
||||
|
||||
|
||||
@skip_unless_feature("jpg_2000")
|
||||
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
|
||||
def test_save_alpha(tmp_path: Path, mode) -> None:
|
||||
def test_save_alpha(tmp_path: Path, mode: str) -> None:
|
||||
helper_save_as_pdf(tmp_path, mode)
|
||||
|
||||
|
||||
|
@ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None:
|
|||
{"dpi": (75, 150), "resolution": 200},
|
||||
),
|
||||
)
|
||||
def test_dpi(params, tmp_path: Path) -> None:
|
||||
def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
|
||||
outfile = str(tmp_path / "temp.pdf")
|
||||
|
@ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None:
|
|||
assert os.path.getsize(outfile) > 0
|
||||
|
||||
# Test appending using a generator
|
||||
def im_generator(ims):
|
||||
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||
yield from ims
|
||||
|
||||
im.save(outfile, save_all=True, append_images=im_generator(ims))
|
||||
|
@ -226,7 +227,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
|
|||
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
|
||||
|
||||
|
||||
def check_pdf_pages_consistency(pdf) -> None:
|
||||
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
|
||||
pages_info = pdf.read_indirect(pdf.pages_ref)
|
||||
assert b"Parent" not in pages_info
|
||||
assert b"Kids" in pages_info
|
||||
|
@ -339,7 +340,7 @@ def test_pdf_append_to_bytesio() -> None:
|
|||
@pytest.mark.timeout(1)
|
||||
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
|
||||
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
|
||||
def test_redos(newline) -> None:
|
||||
def test_redos(newline: bytes) -> None:
|
||||
malicious = b" trailer<<>>" + newline * 3456
|
||||
|
||||
# This particular exception isn't relevant here.
|
||||
|
|
|
@ -6,6 +6,8 @@ import warnings
|
|||
import zlib
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -22,6 +24,7 @@ from .helper import (
|
|||
skip_unless_feature,
|
||||
)
|
||||
|
||||
ElementTree: ModuleType | None
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
|
@ -36,7 +39,7 @@ TEST_PNG_FILE = "Tests/images/hopper.png"
|
|||
MAGIC = PngImagePlugin._MAGIC
|
||||
|
||||
|
||||
def chunk(cid, *data):
|
||||
def chunk(cid: bytes, *data: bytes) -> bytes:
|
||||
test_file = BytesIO()
|
||||
PngImagePlugin.putchunk(*(test_file, cid) + data)
|
||||
return test_file.getvalue()
|
||||
|
@ -52,20 +55,20 @@ HEAD = MAGIC + IHDR
|
|||
TAIL = IDAT + IEND
|
||||
|
||||
|
||||
def load(data):
|
||||
def load(data: bytes) -> Image.Image:
|
||||
return Image.open(BytesIO(data))
|
||||
|
||||
|
||||
def roundtrip(im, **options):
|
||||
def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile:
|
||||
out = BytesIO()
|
||||
im.save(out, "PNG", **options)
|
||||
out.seek(0)
|
||||
return Image.open(out)
|
||||
return cast(PngImagePlugin.PngImageFile, Image.open(out))
|
||||
|
||||
|
||||
@skip_unless_feature("zlib")
|
||||
class TestFilePng:
|
||||
def get_chunks(self, filename):
|
||||
def get_chunks(self, filename: str) -> list[bytes]:
|
||||
chunks = []
|
||||
with open(filename, "rb") as fp:
|
||||
fp.read(8)
|
||||
|
@ -99,7 +102,7 @@ class TestFilePng:
|
|||
im = hopper(mode)
|
||||
im.save(test_file)
|
||||
with Image.open(test_file) as reloaded:
|
||||
if mode in ("I;16", "I;16B"):
|
||||
if mode in ("I", "I;16B"):
|
||||
reloaded = reloaded.convert(mode)
|
||||
assert_image_equal(reloaded, im)
|
||||
|
||||
|
@ -301,8 +304,8 @@ class TestFilePng:
|
|||
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||
|
||||
def test_save_grayscale_transparency(self, tmp_path: Path) -> None:
|
||||
for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
|
||||
in_file = "Tests/images/" + mode.lower() + "_trns.png"
|
||||
for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items():
|
||||
in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png"
|
||||
with Image.open(in_file) as im:
|
||||
assert im.mode == mode
|
||||
assert im.info["transparency"] == 255
|
||||
|
@ -436,7 +439,7 @@ class TestFilePng:
|
|||
def test_unicode_text(self) -> None:
|
||||
# Check preservation of non-ASCII characters
|
||||
|
||||
def rt_text(value) -> None:
|
||||
def rt_text(value: str) -> None:
|
||||
im = Image.new("RGB", (32, 32))
|
||||
info = PngImagePlugin.PngInfo()
|
||||
info.add_text("Text", value)
|
||||
|
@ -616,6 +619,10 @@ class TestFilePng:
|
|||
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
|
||||
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
|
||||
|
||||
def test_unknown_compression_method(self) -> None:
|
||||
with pytest.raises(SyntaxError, match="Unknown compression method"):
|
||||
PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png")
|
||||
|
||||
def test_padded_idat(self) -> None:
|
||||
# This image has been manually hexedited
|
||||
# so that the IDAT chunk has padding at the end
|
||||
|
@ -636,7 +643,7 @@ class TestFilePng:
|
|||
@pytest.mark.parametrize(
|
||||
"cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT")
|
||||
)
|
||||
def test_truncated_chunks(self, cid) -> None:
|
||||
def test_truncated_chunks(self, cid: bytes) -> None:
|
||||
fp = BytesIO()
|
||||
with PngImagePlugin.PngStream(fp) as png:
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -755,7 +762,7 @@ class TestFilePng:
|
|||
im.seek(1)
|
||||
|
||||
@pytest.mark.parametrize("buffer", (True, False))
|
||||
def test_save_stdout(self, buffer) -> None:
|
||||
def test_save_stdout(self, buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
|
@ -780,6 +787,18 @@ class TestFilePng:
|
|||
with Image.open(mystdout) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
|
||||
|
||||
def test_truncated_end_chunk(self) -> None:
|
||||
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
|
||||
@skip_unless_feature("zlib")
|
||||
|
|
|
@ -70,7 +70,9 @@ def test_sanity() -> None:
|
|||
),
|
||||
),
|
||||
)
|
||||
def test_arbitrary_maxval(data, mode, pixels) -> None:
|
||||
def test_arbitrary_maxval(
|
||||
data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...]
|
||||
) -> None:
|
||||
fp = BytesIO(data)
|
||||
with Image.open(fp) as im:
|
||||
assert im.size == (3, 1)
|
||||
|
@ -86,7 +88,7 @@ def test_16bit_pgm() -> None:
|
|||
assert im.size == (20, 100)
|
||||
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png")
|
||||
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff")
|
||||
|
||||
|
||||
def test_16bit_pgm_write(tmp_path: Path) -> None:
|
||||
|
@ -139,7 +141,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None:
|
|||
b"Pf 1 1 -0.0 \0\0\0\0",
|
||||
],
|
||||
)
|
||||
def test_pfm_invalid(data) -> None:
|
||||
def test_pfm_invalid(data: bytes) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
with Image.open(BytesIO(data)):
|
||||
pass
|
||||
|
@ -162,7 +164,7 @@ def test_pfm_invalid(data) -> None:
|
|||
),
|
||||
),
|
||||
)
|
||||
def test_plain(plain_path, raw_path) -> None:
|
||||
def test_plain(plain_path: str, raw_path: str) -> None:
|
||||
with Image.open(plain_path) as im:
|
||||
assert_image_equal_tofile(im, raw_path)
|
||||
|
||||
|
@ -186,7 +188,9 @@ def test_16bit_plain_pgm() -> None:
|
|||
(b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6),
|
||||
),
|
||||
)
|
||||
def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> None:
|
||||
def test_plain_data_with_comment(
|
||||
tmp_path: Path, header: bytes, data: bytes, comment_count: int
|
||||
) -> None:
|
||||
path1 = str(tmp_path / "temp1.ppm")
|
||||
path2 = str(tmp_path / "temp2.ppm")
|
||||
comment = b"# comment" * comment_count
|
||||
|
@ -199,7 +203,7 @@ def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) ->
|
|||
|
||||
|
||||
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
|
||||
def test_plain_truncated_data(tmp_path: Path, data) -> None:
|
||||
def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
@ -210,7 +214,7 @@ def test_plain_truncated_data(tmp_path: Path, data) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
|
||||
def test_plain_invalid_data(tmp_path: Path, data) -> None:
|
||||
def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
@ -227,7 +231,7 @@ def test_plain_invalid_data(tmp_path: Path, data) -> None:
|
|||
b"P3\n128 128\n255\n012345678910 0", # token too long
|
||||
),
|
||||
)
|
||||
def test_plain_ppm_token_too_long(tmp_path: Path, data) -> None:
|
||||
def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
@ -237,13 +241,23 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data) -> None:
|
|||
im.load()
|
||||
|
||||
|
||||
def test_plain_ppm_value_negative(tmp_path: Path) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P3\n128 128\n255\n-1")
|
||||
|
||||
with Image.open(path) as im:
|
||||
with pytest.raises(ValueError, match="Channel value is negative"):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_plain_ppm_value_too_large(tmp_path: Path) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P3\n128 128\n255\n256")
|
||||
|
||||
with Image.open(path) as im:
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="Channel value too large"):
|
||||
im.load()
|
||||
|
||||
|
||||
|
@ -313,7 +327,7 @@ def test_not_enough_image_data(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("maxval", (b"0", b"65536"))
|
||||
def test_invalid_maxval(maxval, tmp_path: Path) -> None:
|
||||
def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P6\n3 1 " + maxval)
|
||||
|
@ -351,7 +365,7 @@ def test_mimetypes(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("buffer", (True, False))
|
||||
def test_save_stdout(buffer) -> None:
|
||||
def test_save_stdout(buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, PsdImagePlugin
|
||||
from PIL import Image, PsdImagePlugin, UnidentifiedImageError
|
||||
|
||||
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
|
||||
|
||||
|
@ -113,6 +113,11 @@ def test_rgba() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
|
||||
|
||||
|
||||
def test_negative_top_left_layer() -> None:
|
||||
with Image.open("Tests/images/negative_top_left_layer.psd") as im:
|
||||
assert im.layers[0][2] == (-50, -50, 50, 50)
|
||||
|
||||
|
||||
def test_layer_skip() -> None:
|
||||
with Image.open("Tests/images/five_channels.psd") as im:
|
||||
assert im.n_frames == 1
|
||||
|
@ -147,17 +152,17 @@ def test_combined_larger_than_size() -> None:
|
|||
[
|
||||
(
|
||||
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
||||
Image.UnidentifiedImageError,
|
||||
UnidentifiedImageError,
|
||||
),
|
||||
(
|
||||
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
||||
Image.UnidentifiedImageError,
|
||||
UnidentifiedImageError,
|
||||
),
|
||||
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
||||
],
|
||||
)
|
||||
def test_crashes(test_file, raises) -> None:
|
||||
def test_crashes(test_file: str, raises) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with pytest.raises(raises):
|
||||
with Image.open(f):
|
||||
|
|
|
@ -72,7 +72,7 @@ def test_invalid_file() -> None:
|
|||
|
||||
|
||||
def test_write(tmp_path: Path) -> None:
|
||||
def roundtrip(img) -> None:
|
||||
def roundtrip(img: Image.Image) -> None:
|
||||
out = str(tmp_path / "temp.sgi")
|
||||
img.save(out, format="sgi")
|
||||
assert_image_equal_tofile(img, out)
|
||||
|
|
|
@ -9,7 +9,7 @@ import pytest
|
|||
|
||||
from PIL import Image, ImageSequence, SpiderImagePlugin
|
||||
|
||||
from .helper import assert_image_equal_tofile, hopper, is_pypy
|
||||
from .helper import assert_image_equal, hopper, is_pypy
|
||||
|
||||
TEST_FILE = "Tests/images/hopper.spider"
|
||||
|
||||
|
@ -160,4 +160,5 @@ def test_odd_size() -> None:
|
|||
im.save(data, format="SPIDER")
|
||||
|
||||
data.seek(0)
|
||||
assert_image_equal_tofile(im, data)
|
||||
with Image.open(data) as im2:
|
||||
assert_image_equal(im, im2)
|
||||
|
|
|
@ -19,7 +19,7 @@ TEST_TAR_FILE = "Tests/images/hopper.tar"
|
|||
("jpg", "hopper.jpg", "JPEG"),
|
||||
),
|
||||
)
|
||||
def test_sanity(codec, test_path, format) -> None:
|
||||
def test_sanity(codec: str, test_path: str, format: str) -> None:
|
||||
if features.check(codec):
|
||||
with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar:
|
||||
with Image.open(tar) as im:
|
||||
|
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
|
@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", _MODES)
|
||||
def test_sanity(mode, tmp_path: Path) -> None:
|
||||
def roundtrip(original_im) -> None:
|
||||
def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||
def roundtrip(original_im: Image.Image) -> None:
|
||||
out = str(tmp_path / "temp.tga")
|
||||
|
||||
original_im.save(out, rle=rle)
|
||||
|
@ -65,6 +65,11 @@ def test_sanity(mode, tmp_path: Path) -> None:
|
|||
roundtrip(original_im)
|
||||
|
||||
|
||||
def test_palette_depth_8(tmp_path: Path) -> None:
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
Image.open("Tests/images/p_8.tga")
|
||||
|
||||
|
||||
def test_palette_depth_16(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/p_16.tga") as im:
|
||||
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png")
|
||||
|
@ -133,6 +138,11 @@ def test_small_palette(tmp_path: Path) -> None:
|
|||
assert reloaded.getpalette() == colors
|
||||
|
||||
|
||||
def test_missing_palette() -> None:
|
||||
with Image.open("Tests/images/dilation4.lut") as im:
|
||||
assert im.mode == "L"
|
||||
|
||||
|
||||
def test_save_wrong_mode(tmp_path: Path) -> None:
|
||||
im = hopper("PA")
|
||||
out = str(tmp_path / "temp.tga")
|
||||
|
|
|
@ -4,6 +4,8 @@ import os
|
|||
import warnings
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -20,6 +22,7 @@ from .helper import (
|
|||
is_win32,
|
||||
)
|
||||
|
||||
ElementTree: ModuleType | None
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
|
@ -110,6 +113,10 @@ class TestFileTiff:
|
|||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||
|
||||
def test_seek_too_large(self):
|
||||
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
||||
Image.open("Tests/images/seek_too_large.tif")
|
||||
|
||||
def test_set_legacy_api(self) -> None:
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
with pytest.raises(Exception) as e:
|
||||
|
@ -156,7 +163,7 @@ class TestFileTiff:
|
|||
"resolution_unit, dpi",
|
||||
[(None, 72.8), (2, 72.8), (3, 184.912)],
|
||||
)
|
||||
def test_load_float_dpi(self, resolution_unit, dpi) -> None:
|
||||
def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None:
|
||||
with Image.open(
|
||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||
) as im:
|
||||
|
@ -284,7 +291,7 @@ class TestFileTiff:
|
|||
("Tests/images/multipage.tiff", 3),
|
||||
),
|
||||
)
|
||||
def test_n_frames(self, path, n_frames) -> None:
|
||||
def test_n_frames(self, path: str, n_frames: int) -> None:
|
||||
with Image.open(path) as im:
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
@ -402,7 +409,7 @@ class TestFileTiff:
|
|||
assert len_before == len_after + 1
|
||||
|
||||
@pytest.mark.parametrize("legacy_api", (False, True))
|
||||
def test_load_byte(self, legacy_api) -> None:
|
||||
def test_load_byte(self, legacy_api: bool) -> None:
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
data = b"abc"
|
||||
ret = ifd.load_byte(data, legacy_api)
|
||||
|
@ -431,7 +438,7 @@ class TestFileTiff:
|
|||
assert 0x8825 in im.tag_v2
|
||||
|
||||
def test_exif(self, tmp_path: Path) -> None:
|
||||
def check_exif(exif) -> None:
|
||||
def check_exif(exif: Image.Exif) -> None:
|
||||
assert sorted(exif.keys()) == [
|
||||
256,
|
||||
257,
|
||||
|
@ -511,7 +518,7 @@ class TestFileTiff:
|
|||
assert im.getexif()[273] == (1408, 1907)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L"))
|
||||
def test_photometric(self, mode, tmp_path: Path) -> None:
|
||||
def test_photometric(self, mode: str, tmp_path: Path) -> None:
|
||||
filename = str(tmp_path / "temp.tif")
|
||||
im = hopper(mode)
|
||||
im.save(filename, tiffinfo={262: 0})
|
||||
|
@ -620,6 +627,7 @@ class TestFileTiff:
|
|||
im.save(outfile, tiffinfo={278: 256})
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2[278] == 256
|
||||
|
||||
def test_strip_raw(self) -> None:
|
||||
|
@ -660,7 +668,7 @@ class TestFileTiff:
|
|||
assert_image_equal_tofile(reloaded, infile)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
def test_palette(self, mode, tmp_path: Path) -> None:
|
||||
def test_palette(self, mode: str, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
||||
im = hopper(mode)
|
||||
|
@ -689,7 +697,7 @@ class TestFileTiff:
|
|||
assert reread.n_frames == 3
|
||||
|
||||
# Test appending using a generator
|
||||
def im_generator(ims):
|
||||
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||
yield from ims
|
||||
|
||||
mp = BytesIO()
|
||||
|
@ -860,7 +868,7 @@ class TestFileTiff:
|
|||
],
|
||||
)
|
||||
@pytest.mark.timeout(2)
|
||||
def test_oom(self, test_file) -> None:
|
||||
def test_oom(self, test_file: str) -> None:
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open(test_file):
|
||||
|
|
|
@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
|
||||
def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
|
||||
def test_writing_other_types_to_ascii(
|
||||
value: bytes | int, expected: str, tmp_path: Path
|
||||
) -> None:
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
tag = TiffTags.TAGS_V2[271]
|
||||
|
@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("value", (1, IFDRational(1)))
|
||||
def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None:
|
||||
def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
|
@ -222,14 +224,17 @@ def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None:
|
|||
assert reloaded.tag_v2[700] == b"\x01"
|
||||
|
||||
|
||||
def test_writing_other_types_to_undefined(tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize("value", (1, IFDRational(1)))
|
||||
def test_writing_other_types_to_undefined(
|
||||
value: int | IFDRational, tmp_path: Path
|
||||
) -> None:
|
||||
im = hopper()
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
tag = TiffTags.TAGS_V2[33723]
|
||||
assert tag.type == TiffTags.UNDEFINED
|
||||
|
||||
info[33723] = 1
|
||||
info[33723] = value
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
im.save(out, tiffinfo=info)
|
||||
|
|
|
@ -151,3 +151,15 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
|
|||
target = im.convert("RGBA")
|
||||
|
||||
assert_image_similar(image, target, 25.0)
|
||||
|
||||
|
||||
def test_alpha_quality(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
out = str(tmp_path / "temp.webp")
|
||||
im.save(out)
|
||||
|
||||
out_quality = str(tmp_path / "quality.webp")
|
||||
im.save(out_quality, alpha_quality=50)
|
||||
with Image.open(out) as reloaded:
|
||||
with Image.open(out_quality) as reloaded_quality:
|
||||
assert reloaded.tobytes() != reloaded_quality.tobytes()
|
||||
|
|
|
@ -188,3 +188,21 @@ def test_seek_errors() -> None:
|
|||
|
||||
with pytest.raises(EOFError):
|
||||
im.seek(42)
|
||||
|
||||
|
||||
def test_alpha_quality(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
first_frame = Image.new("L", im.size)
|
||||
|
||||
out = str(tmp_path / "temp.webp")
|
||||
first_frame.save(out, save_all=True, append_images=[im])
|
||||
|
||||
out_quality = str(tmp_path / "quality.webp")
|
||||
first_frame.save(
|
||||
out_quality, save_all=True, append_images=[im], alpha_quality=50
|
||||
)
|
||||
with Image.open(out) as reloaded:
|
||||
reloaded.seek(1)
|
||||
with Image.open(out_quality) as reloaded_quality:
|
||||
reloaded_quality.seek(1)
|
||||
assert reloaded.tobytes() != reloaded_quality.tobytes()
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -14,6 +15,7 @@ pytestmark = [
|
|||
skip_unless_feature("webp_mux"),
|
||||
]
|
||||
|
||||
ElementTree: ModuleType | None
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL import Image, ImageDraw, ImageFont, _util
|
||||
|
||||
from .helper import PillowLeakTestCase, skip_unless_feature
|
||||
from .helper import PillowLeakTestCase, features, skip_unless_feature
|
||||
|
||||
original_core = ImageFont.core
|
||||
|
||||
|
||||
class TestTTypeFontLeak(PillowLeakTestCase):
|
||||
|
@ -31,5 +33,11 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
|
|||
mem_limit = 1024 # k
|
||||
|
||||
def test_leak(self) -> None:
|
||||
default_font = ImageFont.load_default()
|
||||
if features.check_module("freetype2"):
|
||||
ImageFont.core = _util.DeferredError(ImportError)
|
||||
try:
|
||||
default_font = ImageFont.load_default()
|
||||
finally:
|
||||
ImageFont.core = original_core
|
||||
|
||||
self._test_font(default_font)
|
||||
|
|
|
@ -2,21 +2,18 @@ from __future__ import annotations
|
|||
|
||||
import colorsys
|
||||
import itertools
|
||||
from typing import Callable
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import assert_image_similar, hopper
|
||||
|
||||
|
||||
def int_to_float(i):
|
||||
def int_to_float(i: int) -> float:
|
||||
return i / 255
|
||||
|
||||
|
||||
def str_to_float(i):
|
||||
return ord(i) / 255
|
||||
|
||||
|
||||
def tuple_to_ints(tp):
|
||||
def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]:
|
||||
x, y, z = tp
|
||||
return int(x * 255.0), int(y * 255.0), int(z * 255.0)
|
||||
|
||||
|
@ -25,7 +22,7 @@ def test_sanity() -> None:
|
|||
Image.new("HSV", (100, 100))
|
||||
|
||||
|
||||
def wedge():
|
||||
def wedge() -> Image.Image:
|
||||
w = Image._wedge()
|
||||
w90 = w.rotate(90)
|
||||
|
||||
|
@ -49,7 +46,11 @@ def wedge():
|
|||
return img
|
||||
|
||||
|
||||
def to_xxx_colorsys(im, func, mode):
|
||||
def to_xxx_colorsys(
|
||||
im: Image.Image,
|
||||
func: Callable[[float, float, float], tuple[float, float, float]],
|
||||
mode: str,
|
||||
) -> Image.Image:
|
||||
# convert the hard way using the library colorsys routines.
|
||||
|
||||
(r, g, b) = im.split()
|
||||
|
@ -70,11 +71,11 @@ def to_xxx_colorsys(im, func, mode):
|
|||
return hsv
|
||||
|
||||
|
||||
def to_hsv_colorsys(im):
|
||||
def to_hsv_colorsys(im: Image.Image) -> Image.Image:
|
||||
return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV")
|
||||
|
||||
|
||||
def to_rgb_colorsys(im):
|
||||
def to_rgb_colorsys(im: Image.Image) -> Image.Image:
|
||||
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
|
||||
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import sys
|
|||
import tempfile
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -15,6 +16,7 @@ from PIL import (
|
|||
ExifTags,
|
||||
Image,
|
||||
ImageDraw,
|
||||
ImageFile,
|
||||
ImagePalette,
|
||||
UnidentifiedImageError,
|
||||
features,
|
||||
|
@ -31,41 +33,43 @@ from .helper import (
|
|||
skip_unless_feature,
|
||||
)
|
||||
|
||||
# name, pixel size
|
||||
image_modes = (
|
||||
("1", 1),
|
||||
("L", 1),
|
||||
("LA", 4),
|
||||
("La", 4),
|
||||
("P", 1),
|
||||
("PA", 4),
|
||||
("F", 4),
|
||||
("I", 4),
|
||||
("I;16", 2),
|
||||
("I;16L", 2),
|
||||
("I;16B", 2),
|
||||
("I;16N", 2),
|
||||
("RGB", 4),
|
||||
("RGBA", 4),
|
||||
("RGBa", 4),
|
||||
("RGBX", 4),
|
||||
("BGR;15", 2),
|
||||
("BGR;16", 2),
|
||||
("BGR;24", 3),
|
||||
("CMYK", 4),
|
||||
("YCbCr", 4),
|
||||
("HSV", 4),
|
||||
("LAB", 4),
|
||||
)
|
||||
|
||||
image_mode_names = [name for name, _ in image_modes]
|
||||
|
||||
|
||||
class TestImage:
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
(
|
||||
"1",
|
||||
"P",
|
||||
"PA",
|
||||
"L",
|
||||
"LA",
|
||||
"La",
|
||||
"F",
|
||||
"I",
|
||||
"I;16",
|
||||
"I;16L",
|
||||
"I;16B",
|
||||
"I;16N",
|
||||
"RGB",
|
||||
"RGBX",
|
||||
"RGBA",
|
||||
"RGBa",
|
||||
"BGR;15",
|
||||
"BGR;16",
|
||||
"BGR;24",
|
||||
"CMYK",
|
||||
"YCbCr",
|
||||
"LAB",
|
||||
"HSV",
|
||||
),
|
||||
)
|
||||
def test_image_modes_success(self, mode) -> None:
|
||||
@pytest.mark.parametrize("mode", image_mode_names)
|
||||
def test_image_modes_success(self, mode: str) -> None:
|
||||
Image.new(mode, (1, 1))
|
||||
|
||||
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
|
||||
def test_image_modes_fail(self, mode) -> None:
|
||||
def test_image_modes_fail(self, mode: str) -> None:
|
||||
with pytest.raises(ValueError) as e:
|
||||
Image.new(mode, (1, 1))
|
||||
assert str(e.value) == "unrecognized image mode"
|
||||
|
@ -104,7 +108,7 @@ class TestImage:
|
|||
|
||||
def test_repr_pretty(self) -> None:
|
||||
class Pretty:
|
||||
def text(self, text) -> None:
|
||||
def text(self, text: str) -> None:
|
||||
self.pretty_output = text
|
||||
|
||||
im = Image.new("L", (100, 100))
|
||||
|
@ -141,13 +145,13 @@ class TestImage:
|
|||
assert im.height == 2
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
im.size = (3, 4)
|
||||
im.size = (3, 4) # type: ignore[misc]
|
||||
|
||||
def test_set_mode(self) -> None:
|
||||
im = Image.new("RGB", (1, 1))
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
im.mode = "P"
|
||||
im.mode = "P" # type: ignore[misc]
|
||||
|
||||
def test_invalid_image(self) -> None:
|
||||
im = io.BytesIO(b"")
|
||||
|
@ -166,8 +170,6 @@ class TestImage:
|
|||
pass
|
||||
|
||||
def test_pathlib(self, tmp_path: Path) -> None:
|
||||
from PIL.Image import Path
|
||||
|
||||
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
|
||||
assert im.mode == "P"
|
||||
assert im.size == (10, 10)
|
||||
|
@ -188,7 +190,9 @@ class TestImage:
|
|||
temp_file = str(tmp_path / "temp.jpg")
|
||||
|
||||
class FP:
|
||||
def write(self, b) -> None:
|
||||
name: str
|
||||
|
||||
def write(self, b: bytes) -> None:
|
||||
pass
|
||||
|
||||
fp = FP()
|
||||
|
@ -542,7 +546,7 @@ class TestImage:
|
|||
"PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower"
|
||||
)
|
||||
@pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
|
||||
def test_empty_image(self, size) -> None:
|
||||
def test_empty_image(self, size: tuple[int, int]) -> None:
|
||||
Image.new("RGB", size)
|
||||
|
||||
def test_storage_neg(self) -> None:
|
||||
|
@ -569,7 +573,7 @@ class TestImage:
|
|||
Image.linear_gradient(wrong_mode)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
|
||||
def test_linear_gradient(self, mode) -> None:
|
||||
def test_linear_gradient(self, mode: str) -> None:
|
||||
# Arrange
|
||||
target_file = "Tests/images/linear_gradient.png"
|
||||
|
||||
|
@ -594,7 +598,7 @@ class TestImage:
|
|||
Image.radial_gradient(wrong_mode)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
|
||||
def test_radial_gradient(self, mode) -> None:
|
||||
def test_radial_gradient(self, mode: str) -> None:
|
||||
# Arrange
|
||||
target_file = "Tests/images/radial_gradient.png"
|
||||
|
||||
|
@ -669,7 +673,11 @@ class TestImage:
|
|||
blank_p.palette = None
|
||||
blank_pa.palette = None
|
||||
|
||||
def _make_new(base_image, image, palette_result=None) -> None:
|
||||
def _make_new(
|
||||
base_image: Image.Image,
|
||||
image: Image.Image,
|
||||
palette_result: ImagePalette.ImagePalette | None = None,
|
||||
) -> None:
|
||||
new_image = base_image._new(image.im)
|
||||
assert new_image.mode == image.mode
|
||||
assert new_image.size == image.size
|
||||
|
@ -684,15 +692,18 @@ class TestImage:
|
|||
_make_new(im, blank_p, ImagePalette.ImagePalette())
|
||||
_make_new(im, blank_pa, ImagePalette.ImagePalette())
|
||||
|
||||
def test_p_from_rgb_rgba(self) -> None:
|
||||
for mode, color in [
|
||||
@pytest.mark.parametrize(
|
||||
"mode, color",
|
||||
(
|
||||
("RGB", "#DDEEFF"),
|
||||
("RGB", (221, 238, 255)),
|
||||
("RGBA", (221, 238, 255, 255)),
|
||||
]:
|
||||
im = Image.new("P", (100, 100), color)
|
||||
expected = Image.new(mode, (100, 100), color)
|
||||
assert_image_equal(im.convert(mode), expected)
|
||||
),
|
||||
)
|
||||
def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None:
|
||||
im = Image.new("P", (100, 100), color)
|
||||
expected = Image.new(mode, (100, 100), color)
|
||||
assert_image_equal(im.convert(mode), expected)
|
||||
|
||||
def test_no_resource_warning_on_save(self, tmp_path: Path) -> None:
|
||||
# https://github.com/python-pillow/Pillow/issues/835
|
||||
|
@ -717,7 +728,7 @@ class TestImage:
|
|||
def test_load_on_nonexclusive_multiframe(self) -> None:
|
||||
with open("Tests/images/frozenpond.mpo", "rb") as fp:
|
||||
|
||||
def act(fp) -> None:
|
||||
def act(fp: IO[bytes]) -> None:
|
||||
im = Image.open(fp)
|
||||
im.load()
|
||||
|
||||
|
@ -910,12 +921,12 @@ class TestImage:
|
|||
assert exif.get_ifd(0xA005)
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero_tobytes(self, size) -> None:
|
||||
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||
im = Image.new("RGB", size)
|
||||
assert im.tobytes() == b""
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero_frombytes(self, size) -> None:
|
||||
def test_zero_frombytes(self, size: tuple[int, int]) -> None:
|
||||
Image.frombytes("RGB", size, b"")
|
||||
|
||||
im = Image.new("RGB", size)
|
||||
|
@ -1000,7 +1011,7 @@ class TestImage:
|
|||
"01r_00.pcx",
|
||||
],
|
||||
)
|
||||
def test_overrun(self, path) -> None:
|
||||
def test_overrun(self, path: str) -> None:
|
||||
"""For overrun completeness, test as:
|
||||
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
|
||||
"""
|
||||
|
@ -1027,7 +1038,7 @@ class TestImage:
|
|||
pass
|
||||
assert not hasattr(im, "fp")
|
||||
|
||||
def test_close_graceful(self, caplog) -> None:
|
||||
def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
copy = im.copy()
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
|
@ -1037,25 +1048,49 @@ class TestImage:
|
|||
assert im.fp is None
|
||||
|
||||
|
||||
class MockEncoder:
|
||||
class TestImageBytes:
|
||||
@pytest.mark.parametrize("mode", image_mode_names)
|
||||
def test_roundtrip_bytes_constructor(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
source_bytes = im.tobytes()
|
||||
|
||||
reloaded = Image.frombytes(mode, im.size, source_bytes)
|
||||
assert reloaded.tobytes() == source_bytes
|
||||
|
||||
@pytest.mark.parametrize("mode", image_mode_names)
|
||||
def test_roundtrip_bytes_method(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
source_bytes = im.tobytes()
|
||||
|
||||
reloaded = Image.new(mode, im.size)
|
||||
reloaded.frombytes(source_bytes)
|
||||
assert reloaded.tobytes() == source_bytes
|
||||
|
||||
@pytest.mark.parametrize(("mode", "pixelsize"), image_modes)
|
||||
def test_getdata_putdata(self, mode: str, pixelsize: int) -> None:
|
||||
im = Image.new(mode, (2, 2))
|
||||
source_bytes = bytes(range(im.width * im.height * pixelsize))
|
||||
im.frombytes(source_bytes)
|
||||
|
||||
reloaded = Image.new(mode, im.size)
|
||||
reloaded.putdata(im.getdata())
|
||||
assert_image_equal(im, reloaded)
|
||||
|
||||
|
||||
class MockEncoder(ImageFile.PyEncoder):
|
||||
pass
|
||||
|
||||
|
||||
def mock_encode(*args):
|
||||
encoder = MockEncoder()
|
||||
encoder.args = args
|
||||
return encoder
|
||||
|
||||
|
||||
class TestRegistry:
|
||||
def test_encode_registry(self) -> None:
|
||||
Image.register_encoder("MOCK", mock_encode)
|
||||
Image.register_encoder("MOCK", MockEncoder)
|
||||
assert "MOCK" in Image.ENCODERS
|
||||
|
||||
enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",))
|
||||
|
||||
assert isinstance(enc, MockEncoder)
|
||||
assert enc.args == ("RGB", "args", "extra")
|
||||
assert enc.mode == "RGB"
|
||||
assert enc.args == ("args", "extra")
|
||||
|
||||
def test_encode_registry_fail(self) -> None:
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -4,6 +4,7 @@ import os
|
|||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -13,6 +14,7 @@ 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:
|
||||
|
@ -23,6 +25,7 @@ else:
|
|||
except ImportError:
|
||||
cffi = None
|
||||
|
||||
numpy: ModuleType | None
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
|
@ -71,9 +74,10 @@ class TestImagePutPixel(AccessTest):
|
|||
pix1 = im1.load()
|
||||
pix2 = im2.load()
|
||||
|
||||
for x, y in ((0, "0"), ("0", 0)):
|
||||
with pytest.raises(TypeError):
|
||||
pix1[x, y]
|
||||
with pytest.raises(TypeError):
|
||||
pix1[0, "0"]
|
||||
with pytest.raises(TypeError):
|
||||
pix1["0", 0]
|
||||
|
||||
for y in range(im1.size[1]):
|
||||
for x in range(im1.size[0]):
|
||||
|
@ -123,12 +127,13 @@ class TestImagePutPixel(AccessTest):
|
|||
im = hopper()
|
||||
pix = im.load()
|
||||
|
||||
assert numpy is not None
|
||||
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
|
||||
|
||||
|
||||
class TestImageGetPixel(AccessTest):
|
||||
@staticmethod
|
||||
def color(mode):
|
||||
def color(mode: str) -> int | tuple[int, ...]:
|
||||
bands = Image.getmodebands(mode)
|
||||
if bands == 1:
|
||||
return 1
|
||||
|
@ -138,12 +143,13 @@ class TestImageGetPixel(AccessTest):
|
|||
return (16, 32, 49)
|
||||
return tuple(range(1, bands + 1))
|
||||
|
||||
def check(self, mode, expected_color=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")
|
||||
|
||||
if not expected_color:
|
||||
expected_color = self.color(mode)
|
||||
expected_color = (
|
||||
self.color(mode) if expected_color_int is None else expected_color_int
|
||||
)
|
||||
|
||||
# check putpixel
|
||||
im = Image.new(mode, (1, 1), None)
|
||||
|
@ -222,7 +228,7 @@ class TestImageGetPixel(AccessTest):
|
|||
"YCbCr",
|
||||
),
|
||||
)
|
||||
def test_basic(self, mode) -> None:
|
||||
def test_basic(self, mode: str) -> None:
|
||||
self.check(mode)
|
||||
|
||||
def test_list(self) -> None:
|
||||
|
@ -230,17 +236,15 @@ class TestImageGetPixel(AccessTest):
|
|||
assert im.getpixel([0, 0]) == (20, 20, 70)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
||||
@pytest.mark.parametrize(
|
||||
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
|
||||
)
|
||||
def test_signedness(self, mode, expected_color) -> None:
|
||||
@pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
|
||||
def test_signedness(self, mode: str, expected_color: int) -> None:
|
||||
# see https://github.com/python-pillow/Pillow/issues/452
|
||||
# pixelaccess is using signed int* instead of uint*
|
||||
self.check(mode, expected_color)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
|
||||
def test_p_putpixel_rgb_rgba(self, mode, color) -> None:
|
||||
def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None:
|
||||
im = Image.new(mode, (1, 1))
|
||||
im.putpixel((0, 0), color)
|
||||
|
||||
|
@ -264,7 +268,7 @@ class TestCffiGetPixel(TestImageGetPixel):
|
|||
class TestCffi(AccessTest):
|
||||
_need_cffi_access = True
|
||||
|
||||
def _test_get_access(self, im) -> None:
|
||||
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
|
||||
|
@ -301,7 +305,7 @@ class TestCffi(AccessTest):
|
|||
# im = Image.new('I;32B', (10, 10), 2**10)
|
||||
# self._test_get_access(im)
|
||||
|
||||
def _test_set_access(self, im, color) -> None:
|
||||
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
|
||||
|
@ -361,7 +365,7 @@ class TestCffi(AccessTest):
|
|||
assert px[i, 0] == 0
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
def test_p_putpixel_rgb_rgba(self, mode) -> None:
|
||||
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):
|
||||
|
@ -379,7 +383,7 @@ class TestImagePutPixelError(AccessTest):
|
|||
INVALID_TYPES = ["foo", 1.0, None]
|
||||
|
||||
@pytest.mark.parametrize("mode", IMAGE_MODES1)
|
||||
def test_putpixel_type_error1(self, mode) -> None:
|
||||
def test_putpixel_type_error1(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
for v in self.INVALID_TYPES:
|
||||
with pytest.raises(TypeError, match="color must be int or tuple"):
|
||||
|
@ -402,14 +406,16 @@ class TestImagePutPixelError(AccessTest):
|
|||
),
|
||||
),
|
||||
)
|
||||
def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None:
|
||||
def test_putpixel_invalid_number_of_bands(
|
||||
self, mode: str, band_numbers: tuple[int, ...], match: str
|
||||
) -> None:
|
||||
im = hopper(mode)
|
||||
for band_number in band_numbers:
|
||||
with pytest.raises(TypeError, match=match):
|
||||
im.putpixel((0, 0), (0,) * band_number)
|
||||
|
||||
@pytest.mark.parametrize("mode", IMAGE_MODES2)
|
||||
def test_putpixel_type_error2(self, mode) -> None:
|
||||
def test_putpixel_type_error2(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
for v in self.INVALID_TYPES:
|
||||
with pytest.raises(
|
||||
|
@ -418,7 +424,7 @@ class TestImagePutPixelError(AccessTest):
|
|||
im.putpixel((0, 0), v)
|
||||
|
||||
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
|
||||
def test_putpixel_overflow_error(self, mode) -> None:
|
||||
def test_putpixel_overflow_error(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
with pytest.raises(OverflowError):
|
||||
im.putpixel((0, 0), 2**80)
|
||||
|
@ -430,7 +436,7 @@ class TestEmbeddable:
|
|||
def test_embeddable(self) -> None:
|
||||
import ctypes
|
||||
|
||||
from setuptools.command.build_ext import new_compiler
|
||||
from setuptools.command import build_ext
|
||||
|
||||
with open("embed_pil.c", "w", encoding="utf-8") as fh:
|
||||
fh.write(
|
||||
|
@ -459,7 +465,7 @@ int main(int argc, char* argv[])
|
|||
% sys.prefix.replace("\\", "\\\\")
|
||||
)
|
||||
|
||||
compiler = new_compiler()
|
||||
compiler = getattr(build_ext, "new_compiler")()
|
||||
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
|
||||
|
||||
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
|
||||
|
@ -473,7 +479,7 @@ int main(int argc, char* argv[])
|
|||
env["PATH"] = sys.prefix + ";" + env["PATH"]
|
||||
|
||||
# do not display the Windows Error Reporting dialog
|
||||
ctypes.windll.kernel32.SetErrorMode(0x0002)
|
||||
getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002)
|
||||
|
||||
process = subprocess.Popen(["embed_pil.exe"], env=env)
|
||||
process.communicate()
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
|
@ -13,7 +15,7 @@ im = hopper().resize((128, 100))
|
|||
|
||||
|
||||
def test_toarray() -> None:
|
||||
def test(mode):
|
||||
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
|
||||
ai = numpy.array(im.convert(mode))
|
||||
return ai.shape, ai.dtype.str, ai.nbytes
|
||||
|
||||
|
@ -50,14 +52,14 @@ def test_fromarray() -> None:
|
|||
class Wrapper:
|
||||
"""Class with API matching Image.fromarray"""
|
||||
|
||||
def __init__(self, img, arr_params) -> None:
|
||||
def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None:
|
||||
self.img = img
|
||||
self.__array_interface__ = arr_params
|
||||
|
||||
def tobytes(self):
|
||||
def tobytes(self) -> bytes:
|
||||
return self.img.tobytes()
|
||||
|
||||
def test(mode):
|
||||
def test(mode: str) -> tuple[str, tuple[int, int], bool]:
|
||||
i = im.convert(mode)
|
||||
a = numpy.array(i)
|
||||
# Make wrapper instance for image, new array interface
|
||||
|
|
|
@ -183,6 +183,14 @@ def test_trns_RGB(tmp_path: Path) -> None:
|
|||
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone
|
||||
im_l.save(f)
|
||||
|
||||
im_la = im.convert("LA")
|
||||
assert "transparency" not in im_la.info
|
||||
im_la.save(f)
|
||||
|
||||
im_la = im.convert("La")
|
||||
assert "transparency" not in im_la.info
|
||||
assert im_la.getpixel((0, 0)) == (0, 0)
|
||||
|
||||
im_p = im.convert("P")
|
||||
assert "transparency" in im_p.info
|
||||
im_p.save(f)
|
||||
|
@ -191,6 +199,10 @@ def test_trns_RGB(tmp_path: Path) -> None:
|
|||
assert "transparency" not in im_rgba.info
|
||||
im_rgba.save(f)
|
||||
|
||||
im_rgba = im.convert("RGBa")
|
||||
assert "transparency" not in im_rgba.info
|
||||
assert im_rgba.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
|
||||
im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE)
|
||||
assert "transparency" not in im_p.info
|
||||
im_p.save(f)
|
||||
|
|
|
@ -7,7 +7,12 @@ from .helper import fromstring, skip_unless_feature, tostring
|
|||
pytestmark = skip_unless_feature("jpg")
|
||||
|
||||
|
||||
def draft_roundtrip(in_mode, in_size, req_mode, req_size):
|
||||
def draft_roundtrip(
|
||||
in_mode: str,
|
||||
in_size: tuple[int, int],
|
||||
req_mode: str | None,
|
||||
req_size: tuple[int, int] | None,
|
||||
) -> Image.Image:
|
||||
im = Image.new(in_mode, in_size)
|
||||
data = tostring(im, "JPEG")
|
||||
im = fromstring(data)
|
||||
|
|
|
@ -4,7 +4,7 @@ from .helper import hopper
|
|||
|
||||
|
||||
def test_entropy() -> None:
|
||||
def entropy(mode):
|
||||
def entropy(mode: str) -> float:
|
||||
return hopper(mode).entropy()
|
||||
|
||||
assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0
|
||||
|
|
|
@ -36,7 +36,7 @@ from .helper import assert_image_equal, hopper
|
|||
),
|
||||
)
|
||||
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
|
||||
def test_sanity(filter_to_apply, mode) -> None:
|
||||
def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
|
||||
out = im.filter(filter_to_apply)
|
||||
|
@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
|
||||
def test_sanity_error(mode) -> None:
|
||||
def test_sanity_error(mode: str) -> None:
|
||||
with pytest.raises(TypeError):
|
||||
im = hopper(mode)
|
||||
im.filter("hello")
|
||||
|
@ -53,7 +53,7 @@ def test_sanity_error(mode) -> None:
|
|||
|
||||
# crashes on small images
|
||||
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
|
||||
def test_crash(size) -> None:
|
||||
def test_crash(size: tuple[int, int]) -> None:
|
||||
im = Image.new("RGB", size)
|
||||
im.filter(ImageFilter.SMOOTH)
|
||||
|
||||
|
@ -67,7 +67,10 @@ def test_crash(size) -> None:
|
|||
("RGB", ((4, 0, 0), (0, 0, 0))),
|
||||
),
|
||||
)
|
||||
def test_modefilter(mode, expected) -> None:
|
||||
def test_modefilter(
|
||||
mode: str,
|
||||
expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]],
|
||||
) -> None:
|
||||
im = Image.new(mode, (3, 3), None)
|
||||
im.putdata(list(range(9)))
|
||||
# image is:
|
||||
|
@ -90,7 +93,13 @@ def test_modefilter(mode, expected) -> None:
|
|||
("F", (0.0, 4.0, 8.0)),
|
||||
),
|
||||
)
|
||||
def test_rankfilter(mode, expected) -> None:
|
||||
def test_rankfilter(
|
||||
mode: str,
|
||||
expected: (
|
||||
tuple[float, float, float]
|
||||
| tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]
|
||||
),
|
||||
) -> None:
|
||||
im = Image.new(mode, (3, 3), None)
|
||||
im.putdata(list(range(9)))
|
||||
# image is:
|
||||
|
@ -106,7 +115,7 @@ def test_rankfilter(mode, expected) -> None:
|
|||
@pytest.mark.parametrize(
|
||||
"filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)
|
||||
)
|
||||
def test_rankfilter_error(filter) -> None:
|
||||
def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
im = Image.new("P", (3, 3), None)
|
||||
im.putdata(list(range(9)))
|
||||
|
@ -137,11 +146,9 @@ def test_kernel_not_enough_coefficients() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
|
||||
def test_consistency_3x3(mode) -> None:
|
||||
def test_consistency_3x3(mode: str) -> None:
|
||||
with Image.open("Tests/images/hopper.bmp") as source:
|
||||
reference_name = "hopper_emboss"
|
||||
reference_name += "_I.png" if mode == "I" else ".bmp"
|
||||
with Image.open("Tests/images/" + reference_name) as reference:
|
||||
with Image.open("Tests/images/hopper_emboss.bmp") as reference:
|
||||
kernel = ImageFilter.Kernel(
|
||||
(3, 3),
|
||||
# fmt: off
|
||||
|
@ -151,23 +158,13 @@ def test_consistency_3x3(mode) -> None:
|
|||
# fmt: on
|
||||
0.3,
|
||||
)
|
||||
source = source.split() * 2
|
||||
reference = reference.split() * 2
|
||||
|
||||
if mode == "I":
|
||||
source = source[0].convert(mode)
|
||||
else:
|
||||
source = Image.merge(mode, source[: len(mode)])
|
||||
reference = Image.merge(mode, reference[: len(mode)])
|
||||
assert_image_equal(source.filter(kernel), reference)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
|
||||
def test_consistency_5x5(mode) -> None:
|
||||
def test_consistency_5x5(mode: str) -> None:
|
||||
with Image.open("Tests/images/hopper.bmp") as source:
|
||||
reference_name = "hopper_emboss_more"
|
||||
reference_name += "_I.png" if mode == "I" else ".bmp"
|
||||
with Image.open("Tests/images/" + reference_name) as reference:
|
||||
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
|
||||
kernel = ImageFilter.Kernel(
|
||||
(5, 5),
|
||||
# fmt: off
|
||||
|
@ -179,14 +176,6 @@ def test_consistency_5x5(mode) -> None:
|
|||
# fmt: on
|
||||
0.3,
|
||||
)
|
||||
source = source.split() * 2
|
||||
reference = reference.split() * 2
|
||||
|
||||
if mode == "I":
|
||||
source = source[0].convert(mode)
|
||||
else:
|
||||
source = Image.merge(mode, source[: len(mode)])
|
||||
reference = Image.merge(mode, reference[: len(mode)])
|
||||
assert_image_equal(source.filter(kernel), reference)
|
||||
|
||||
|
||||
|
@ -199,7 +188,7 @@ def test_consistency_5x5(mode) -> None:
|
|||
(2, -2),
|
||||
),
|
||||
)
|
||||
def test_invalid_box_blur_filter(radius) -> None:
|
||||
def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ImageFilter.BoxBlur(radius)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from .helper import assert_image_equal, hopper
|
|||
|
||||
|
||||
@pytest.mark.parametrize("data_type", ("bytes", "memoryview"))
|
||||
def test_sanity(data_type) -> None:
|
||||
def test_sanity(data_type: str) -> None:
|
||||
im1 = hopper()
|
||||
|
||||
data = im1.tobytes()
|
||||
|
|