Merge branch 'main' into jpeg-app-segments

This commit is contained in:
Andrew Murray 2024-07-26 22:22:39 +10:00 committed by GitHub
commit 604c1cf8fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
453 changed files with 13571 additions and 7598 deletions

View File

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

View File

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

View File

@ -1 +1 @@
cibuildwheel==2.16.5
cibuildwheel==2.19.2

View File

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

View File

@ -3,12 +3,13 @@
BasedOnStyle: Google
AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false
AlignAfterOpenBracket: AlwaysBreak
AlignAfterOpenBracket: BlockIndent
BinPackArguments: false
BinPackParameters: false
BreakBeforeBraces: Attach
ColumnLimit: 88
DerivePointerAlignment: false
IndentGotoLabels: false
IndentWidth: 4
Language: Cpp
PointerAlignment: Right

View File

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

2
.github/FUNDING.yml vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,13 +26,16 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs:
build:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
python-minor-version: [8, 9]
python-minor-version: [9]
timeout-minutes: 40
@ -47,11 +50,12 @@ jobs:
uses: actions/checkout@v4
- name: Install Cygwin
uses: egor-tensin/setup-cygwin@v4
uses: cygwin/cygwin-install-action@v4
with:
packages: >
gcc-g++
ghostscript
git
ImageMagick
jpeg
libfreetype-devel
@ -68,8 +72,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
python3${{ matrix.python-minor-version }}-numpy
@ -86,21 +88,15 @@ jobs:
- 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: |
@ -110,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: |
@ -141,11 +132,12 @@ jobs:
bash.exe .ci/after_success.sh
- name: Upload coverage
uses: codecov/codecov-action@v3.1.5
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: GHA_Cygwin
name: Cygwin Python 3.${{ matrix.python-minor-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:

View File

@ -36,32 +36,29 @@ jobs:
docker: [
# Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-22.04-jammy-arm64v8,
ubuntu-22.04-jammy-ppc64le,
ubuntu-22.04-jammy-s390x,
ubuntu-24.04-noble-ppc64le,
ubuntu-24.04-noble-s390x,
# Then run the remainder
alpine,
amazon-2-amd64,
amazon-2023-amd64,
arch,
centos-7-amd64,
centos-stream-8-amd64,
centos-stream-9-amd64,
debian-11-bullseye-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-38-amd64,
fedora-39-amd64,
fedora-40-amd64,
gentoo,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64,
]
dockerTag: [main]
include:
- docker: "ubuntu-22.04-jammy-arm64v8"
qemu-arch: "aarch64"
- docker: "ubuntu-22.04-jammy-ppc64le"
- docker: "ubuntu-24.04-noble-ppc64le"
qemu-arch: "ppc64le"
- docker: "ubuntu-22.04-jammy-s390x"
- docker: "ubuntu-24.04-noble-s390x"
qemu-arch: "s390x"
name: ${{ matrix.docker }}
@ -83,8 +80,8 @@ jobs:
- name: Docker build
run: |
# The Pillow user in the docker container is UID 1000
sudo chown -R 1000 $GITHUB_WORKSPACE
# The Pillow user in the docker container is UID 1001
sudo chown -R 1001 $GITHUB_WORKSPACE
docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE
@ -101,11 +98,12 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage
uses: codecov/codecov-action@v3.1.5
uses: codecov/codecov-action@v4
with:
flags: GHA_Docker
name: ${{ matrix.docker }}
gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs:
build:
runs-on: windows-latest
@ -61,13 +64,12 @@ jobs:
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
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,8 +84,9 @@ jobs:
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage
uses: codecov/codecov-action@v3.1.5
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: GHA_Windows
name: "MSYS2 MinGW"
token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -50,7 +50,7 @@ jobs:
- name: Build and Run Valgrind
run: |
# The Pillow user in the docker container is UID 1000
sudo chown -R 1000 $GITHUB_WORKSPACE
# The Pillow user in the docker container is UID 1001
sudo chown -R 1001 $GITHUB_WORKSPACE
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE

View File

@ -26,13 +26,16 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs:
build:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["pypy3.10", "pypy3.9", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30
@ -66,8 +69,16 @@ jobs:
- name: Print build system information
run: python3 .github/workflows/system-info.py
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
- name: Install Python dependencies
run: >
python3 -m pip install
coverage>=7.4.2
defusedxml
olefile
pyroma
pytest
pytest-cov
pytest-timeout
- name: Install dependencies
id: install
@ -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.1 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images
@ -202,11 +213,12 @@ jobs:
shell: pwsh
- name: Upload coverage
uses: codecov/codecov-action@v3.1.5
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:

View File

@ -27,6 +27,7 @@ concurrency:
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs:
@ -47,33 +48,26 @@ jobs:
"3.11",
"3.10",
"3.9",
"3.8",
]
include:
- python-version: "3.11"
PYTHONOPTIMIZE: 1
REVERSE: "--reverse"
- python-version: "3.10"
PYTHONOPTIMIZE: 2
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true }
# M1 only available for 3.10+
- os: "macos-latest"
python-version: "3.9"
- os: "macos-latest"
python-version: "3.8"
- { os: "macos-13", python-version: "3.9" }
exclude:
- os: "macos-14"
python-version: "3.9"
- os: "macos-14"
python-version: "3.8"
- { os: "macos-14", python-version: "3.9" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
if: "${{ !matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@ -82,6 +76,18 @@ jobs:
".ci/*.sh"
"pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.1.0
if: "${{ matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
nogil: ${{ matrix.disable-gil }}
- name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}"
run: |
echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Build system information
run: python3 .github/workflows/system-info.py
@ -149,11 +155,12 @@ jobs:
.ci/after_success.sh
- name: Upload coverage
uses: codecov/codecov-action@v3.1.5
uses: codecov/codecov-action@v4
with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:

View File

@ -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.5.0
LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.3
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
LIBWEBP_VERSION=1.4.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.16
LIBXCB_VERSION=1.17.0
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
@ -70,13 +70,13 @@ function build {
fi
build_new_zlib
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
build_simple 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
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
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
@ -87,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"
@ -128,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
# libtiff and libxcb cause a conflict with building libtiff and libxcb
# libxau and libxdmcp cause an issue on macOS < 11
# if php is installed, brew tries to reinstall these after installing openblas
# remove cairo to fix building harfbuzz on arm64
# remove lcms2 and libpng to fix building openjpeg on arm64
# remove zstd to avoid inclusion on x86_64
# 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 libxau 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

View File

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

View File

@ -1,10 +1,19 @@
name: Wheels
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
- cron: "42 1 * * 0,3"
push:
paths:
- ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- "setup.py"
- "wheels/*"
- "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake"
@ -14,6 +23,7 @@ on:
paths:
- ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- "setup.py"
- "wheels/*"
- "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake"
@ -31,6 +41,7 @@ env:
jobs:
build-1-QEMU-emulated-wheels:
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest
strategy:
@ -39,11 +50,8 @@ jobs:
python-version:
- pp39
- pp310
- cp38
- cp39
- cp310
- cp311
- cp312
- cp3{9,10,11}
- cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
@ -78,6 +86,7 @@ jobs:
CIBW_ARCHS: "aarch64"
# Likewise, select only one Python version per job to speed this up.
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
CIBW_PRERELEASE_PYTHONS: True
# Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
@ -88,6 +97,7 @@ jobs:
path: ./wheelhouse/*.whl
build-2-native-wheels:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
@ -95,11 +105,11 @@ jobs:
matrix:
include:
- name: "macOS x86_64"
os: macos-latest
os: macos-13
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"
@ -129,10 +139,10 @@ jobs:
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_FREE_THREADED_SUPPORT: True
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-macosx_arm64"
CIBW_PRERELEASE_PYTHONS: True
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4
@ -141,6 +151,7 @@ jobs:
path: ./wheelhouse/*.whl
windows:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest
strategy:
@ -202,7 +213,8 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_SKIP: pp38-*
CIBW_FREE_THREADED_SUPPORT: True
CIBW_PRERELEASE_PYTHONS: True
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow
@ -226,6 +238,7 @@ jobs:
path: winbuild\build\bin\fribidi*
sdist:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -244,8 +257,25 @@ jobs:
name: dist-sdist
path: dist/*.tar.gz
scientific-python-nightly-wheels-publish:
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [build-2-native-wheels, windows]
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v4
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
pypi-publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
runs-on: ubuntu-latest
name: Upload release to PyPI

View File

@ -1,35 +1,42 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0
rev: v0.5.0
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: 24.1.1
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.7.7
rev: 1.7.9
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$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.8
hooks:
- id: clang-format
types: [c]
exclude: ^src/thirdparty/
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
@ -42,18 +49,25 @@ repos:
- id: trailing-whitespace
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.6
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.7.0
rev: 2.1.3
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.16
rev: v0.18
hooks:
- id: validate-pyproject
@ -62,5 +76,10 @@ repos:
hooks:
- id: tox-ini-fmt
- repo: meta
hooks:
- id: check-hooks-apply
- id: check-useless-excludes
ci:
autoupdate_schedule: monthly

View File

@ -3,9 +3,13 @@ version: 2
formats: [pdf]
build:
os: ubuntu-22.04
os: ubuntu-lts-latest
tools:
python: "3"
jobs:
post_checkout:
- git remote add upstream https://github.com/python-pillow/Pillow.git # For forks
- git fetch upstream --tags
python:
install:

View File

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

View File

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

View File

@ -2,7 +2,6 @@
.PHONY: clean
clean:
python3 setup.py clean
rm src/PIL/*.so || true
rm -r build || true
find . -name __pycache__ | xargs rm -r || true
@ -78,8 +77,6 @@ release-test:
python3 selftest.py
python3 -m pytest Tests
python3 -m pip install .
-rm dist/*.egg
-rmdir dist
python3 -m pytest -qq
python3 -m check_manifest
python3 -m pyroma .
@ -121,3 +118,8 @@ lint-fix:
python3 -m black .
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m ruff --fix .
.PHONY: mypy
mypy:
python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
python3 -m tox -e mypy

View File

@ -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"
@ -104,7 +101,7 @@ The core image library is designed for fast access to data stored in a few basic
## More Information
- [Documentation](https://pillow.readthedocs.io/)
- [Installation](https://pillow.readthedocs.io/en/latest/installation.html)
- [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html)
- [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
- [Issues](https://github.com/python-pillow/Pillow/issues)

View File

@ -20,8 +20,10 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
git tag 5.2.0
git push --tags
```
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases).
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
```bash
@ -50,7 +52,9 @@ Released as needed for security, installation or critical bug fixes.
```bash
make sdist
```
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash
git push
@ -72,21 +76,17 @@ Released as needed privately to individual vendors for critical security-related
git tag 2.5.3
git push origin --tags
```
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash
git push origin 2.5.x
```
## Source and Binary Distributions
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
## 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

View File

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

View File

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

View File

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

View File

@ -11,13 +11,15 @@ import subprocess
import sys
import sysconfig
import tempfile
from collections.abc import Sequence
from functools import lru_cache
from io import BytesIO
from typing import Any, Callable, Sequence
from typing import Any, Callable
import pytest
from packaging.version import parse as parse_version
from PIL import Image, ImageMath, features
from PIL import Image, ImageFile, ImageMath, features
logger = logging.getLogger(__name__)
@ -58,9 +60,7 @@ def convert_to_comparable(
return new_a, new_b
def assert_deep_equal(
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
) -> None:
def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:
@ -114,7 +114,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])
@ -171,12 +173,13 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator:
if not features.check(feature):
version = features.version(feature)
if version is None:
return pytest.mark.skip(f"{feature} not available")
if reason is None:
reason = f"{feature} is older than {required}"
version_required = parse_version(required)
version_available = parse_version(features.version(feature))
version_available = parse_version(version)
return pytest.mark.skipif(version_available < version_required, reason=reason)
@ -186,12 +189,13 @@ def mark_if_feature_version(
version_blacklist: str,
reason: str | None = None,
) -> pytest.MarkDecorator:
if not features.check(feature):
version = features.version(feature)
if version is None:
return pytest.mark.pil_noop_mark()
if reason is None:
reason = f"{feature} is {version_blacklist}"
version_required = parse_version(version_blacklist)
version_available = parse_version(features.version(feature))
version_available = parse_version(version)
if (
version_available.major == version_required.major
and version_available.minor == version_required.minor
@ -217,16 +221,11 @@ class PillowLeakTestCase:
from resource import RUSAGE_SELF, getrusage
mem = getrusage(RUSAGE_SELF).ru_maxrss
if sys.platform == "darwin":
# man 2 getrusage:
# ru_maxrss
# This is the maximum resident set size utilized (in bytes).
return mem / 1024 # Kb
# linux
# man 2 getrusage
# ru_maxrss (since Linux 2.6.32)
# This is the maximum resident set size used (in kilobytes).
return mem # Kb
# man 2 getrusage:
# ru_maxrss
# This is the maximum resident set size utilized
# in bytes on macOS, in kilobytes on Linux
return mem / 1024 if sys.platform == "darwin" else mem
def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage()
@ -240,7 +239,7 @@ class PillowLeakTestCase:
# helpers
def fromstring(data: bytes) -> Image.Image:
def fromstring(data: bytes) -> ImageFile.ImageFile:
return Image.open(BytesIO(data))
@ -250,25 +249,38 @@ def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
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()
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
im = im.convert(mode)
else:
try:
im = im.convert(mode)
except ImportError:
if mode == "LAB":
im = Image.open("Tests/images/hopper.Lab.tif")
else:
raise
return im
def djpeg_available() -> bool:
@ -351,7 +363,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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

BIN
Tests/images/2422.flc Normal file

Binary file not shown.

BIN
Tests/images/9bit.j2k Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

BIN
Tests/images/m13.fits Normal file

Binary file not shown.

366
Tests/images/m13_gzip.fits Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 414 B

BIN
Tests/images/p_8.tga Normal file

Binary file not shown.

BIN
Tests/images/rgba16.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
Tests/images/ultrahdr.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

View File

@ -7,13 +7,14 @@ 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"):
pytest.skip("Fuzzer is linux only", allow_module_level=True)
if features.check("libjpeg_turbo"):
version = packaging.version.parse(features.version("libjpeg_turbo"))
libjpeg_turbo_version = features.version("libjpeg_turbo")
if libjpeg_turbo_version is not None:
version = packaging.version.parse(libjpeg_turbo_version)
if version.major == 2 and version.minor == 0:
pytestmark = pytest.mark.valgrind_known_error(
reason="Known failing with libjpeg_turbo 2.0"
@ -43,7 +44,7 @@ def test_fuzz_images(path: str) -> None:
except (
Image.DecompressionBombError,
Image.DecompressionBombWarning,
Image.UnidentifiedImageError,
UnidentifiedImageError,
):
# Known Image.* exceptions
assert True

View File

@ -44,6 +44,9 @@ def test_questionable() -> None:
"pal8os2sp.bmp",
"pal8rletrns.bmp",
"rgb32bf-xbgr.bmp",
"rgba32.bmp",
"rgb32h52.bmp",
"rgba32h56.bmp",
]
for f in get_files("q"):
try:

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import io
import re
from typing import Callable
import pytest
@ -29,13 +30,17 @@ 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], str | None]) -> None:
version = features.version(name)
if not features.check(name):
assert version is None
else:
assert function(name) == version
if name != "PIL":
if name == "zlib" and version is not None:
version = re.sub(".zlib-ng$", "", version)
elif name == "libtiff" and version is not None:
version = re.sub("t$", "", version)
assert version is None or re.search(r"\d+(\.\d+)*$", version)
for module in features.modules:
@ -64,21 +69,25 @@ def test_webp_anim() -> None:
@skip_unless_feature("libjpeg_turbo")
def test_libjpeg_turbo_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo"))
version = features.version("libjpeg_turbo")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@skip_unless_feature("libimagequant")
def test_libimagequant_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
version = features.version("libimagequant")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@pytest.mark.parametrize("feature", features.modules)
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 +125,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: bool) -> 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 +138,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 +157,4 @@ def test_pilinfo() -> None:
+ "-" * 68
+ "\n"
)
assert jpeg in out
assert supported_formats == (jpeg in out)

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from PIL import BmpImagePlugin, Image
from PIL import BmpImagePlugin, Image, _binary
from .helper import (
assert_image_equal,
@ -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")
@ -128,6 +128,29 @@ def test_load_dib() -> None:
assert_image_equal_tofile(im, "Tests/images/clipboard_target.png")
@pytest.mark.parametrize(
"header_size, path",
(
(12, "g/pal8os2.bmp"),
(40, "g/pal1.bmp"),
(52, "q/rgb32h52.bmp"),
(56, "q/rgba32h56.bmp"),
(64, "q/pal8os2v2.bmp"),
(108, "g/pal8v4.bmp"),
(124, "g/pal8v5.bmp"),
),
)
def test_dib_header_size(header_size: int, path: str) -> None:
image_path = "Tests/images/bmp/" + path
with open(image_path, "rb") as fp:
data = fp.read()[14:]
assert _binary.i32le(data) == header_size
dib = io.BytesIO(data)
with Image.open(dib) as im:
im.load()
def test_save_dib(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.dib")
@ -194,7 +217,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:

View File

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

View File

@ -21,9 +21,23 @@ def test_isatty() -> None:
assert container.isatty() is False
def test_seek_mode_0() -> None:
def test_seekable() -> None:
with hopper() as im:
container = ContainerIO.ContainerIO(im, 0, 0)
assert container.seekable() is True
@pytest.mark.parametrize(
"mode, expected_position",
(
(0, 33),
(1, 66),
(2, 100),
),
)
def test_seek_mode(mode: int, expected_position: int) -> None:
# Arrange
mode = 0
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@ -32,35 +46,15 @@ def test_seek_mode_0() -> None:
container.seek(33, mode)
# Assert
assert container.tell() == 33
assert container.tell() == expected_position
def test_seek_mode_1() -> None:
# Arrange
mode = 1
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readable(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
# 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.readable() is True
@pytest.mark.parametrize("bytesmode", (True, False))
@ -70,7 +64,7 @@ def test_read_n0(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(81)
assert container.seek(81) == 81
data = container.read()
# Assert
@ -86,7 +80,7 @@ def test_read_n(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(81)
assert container.seek(81) == 81
data = container.read(3)
# Assert
@ -102,7 +96,7 @@ def test_read_eof(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(100)
assert container.seek(100) == 100
data = container.read()
# Assert
@ -113,21 +107,65 @@ def test_read_eof(bytesmode: bool) -> None:
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readline(bytesmode: bool) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readline()
# Assert
if bytesmode:
data = data.decode()
assert data == "This is line 1\n"
data = container.readline(4)
if bytesmode:
data = data.decode()
assert data == "This"
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readlines(bytesmode: bool) -> None:
expected = [
"This is line 1\n",
"This is line 2\n",
"This is line 3\n",
"This is line 4\n",
"This is line 5\n",
"This is line 6\n",
"This is line 7\n",
"This is line 8\n",
]
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
data = container.readlines()
if bytesmode:
data = [line.decode() for line in data]
assert data == expected
assert container.seek(0) == 0
data = container.readlines(2)
if bytesmode:
data = [line.decode() for line in data]
assert data == expected[:2]
@pytest.mark.parametrize("bytesmode", (True, False))
def test_write(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert container.writable() is False
with pytest.raises(NotImplementedError):
container.write(b"" if bytesmode else "")
with pytest.raises(NotImplementedError):
container.writelines([])
with pytest.raises(NotImplementedError):
container.truncate()
@pytest.mark.parametrize("bytesmode", (True, False))
def test_iter(bytesmode: bool) -> None:
# Arrange
expected = [
"This is line 1\n",
@ -143,9 +181,21 @@ def test_readlines(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readlines()
data = []
for line in container:
data.append(line)
# Assert
if bytesmode:
data = [line.decode() for line in data]
assert data == expected
@pytest.mark.parametrize("bytesmode", (True, False))
def test_file(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert isinstance(container.fileno(), int)
container.flush()
container.close()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,6 +57,7 @@ def test_getiptcinfo_fotostation() -> None:
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
assert iptc is not None
for tag in iptc.keys():
if tag[0] == 240:
return
@ -98,7 +99,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

View File

@ -6,7 +6,7 @@ import warnings
from io import BytesIO
from pathlib import Path
from types import ModuleType
from typing import Any
from typing import Any, cast
import pytest
@ -45,14 +45,20 @@ TEST_FILE = "Tests/images/hopper.jpg"
@skip_unless_feature("jpg")
class TestFileJpeg:
def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image:
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 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
@ -64,7 +70,9 @@ class TestFileJpeg:
def test_sanity(self) -> None:
# internal version number
assert re.search(r"\d+\.\d+$", features.version_codec("jpg"))
version = features.version_codec("jpg")
assert version is not None
assert re.search(r"\d+\.\d+$", version)
with Image.open(TEST_FILE) as im:
im.load()
@ -171,7 +179,7 @@ class TestFileJpeg:
assert k > 0.9
def test_rgb(self) -> None:
def getchannels(im: Image.Image) -> tuple[int, int, int]:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]:
return tuple(v[0] for v in im.layer)
im = hopper()
@ -188,7 +196,7 @@ class TestFileJpeg:
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
)
def test_dpi(self, test_image_path: str) -> None:
def test(xdpi: int, ydpi: int | None = None):
def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None:
with Image.open(test_image_path) as im:
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
return im.info.get("dpi")
@ -271,13 +279,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
@ -287,15 +295,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")
@ -366,6 +374,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) == {}
@ -444,14 +453,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())
@ -459,7 +468,9 @@ class TestFileJpeg:
assert_image(im1, im2.mode, im2.size)
def test_subsampling(self) -> None:
def getsampling(im: Image.Image):
def getsampling(
im: JpegImagePlugin.JpegImageFile,
) -> tuple[int, int, int, int, int, int]:
layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
@ -715,7 +726,7 @@ class TestFileJpeg:
def test_save_cjpeg(self, tmp_path: Path) -> None:
with Image.open(TEST_FILE) as img:
tempfile = str(tmp_path / "temp.jpg")
JpegImagePlugin._save_cjpeg(img, 0, tempfile)
JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17)
@ -886,7 +897,7 @@ class TestFileJpeg:
def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im:
assert im.info["exif"] == b"Exif\x00\x00firstsecond"
assert im.getexif()[270] == "firstsecond"
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@ -933,24 +944,25 @@ class TestFileJpeg:
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile"
def test_jpeg_magic_number(self) -> None:
def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer.max_pos = 0
max_pos = 0
orig_read = buffer.read
def read(n=-1):
def read(n: int | None = -1) -> bytes:
nonlocal max_pos
res = orig_read(n)
buffer.max_pos = max(buffer.max_pos, buffer.tell())
max_pos = max(max_pos, buffer.tell())
return res
buffer.read = read
monkeypatch.setattr(buffer, "read", read)
with pytest.raises(UnidentifiedImageError):
with Image.open(buffer):
pass
# Assert the entire file has not been read
assert 0 < buffer.max_pos < size
assert 0 < max_pos < size
def test_getxmp(self) -> None:
with Image.open("Tests/images/xmp_test.jpg") as im:
@ -961,6 +973,7 @@ class TestFileJpeg:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
@ -1011,13 +1024,7 @@ class TestFileJpeg:
def decode(self, buffer: bytes) -> tuple[int, int]:
return 0, 0
decoder = InfiniteMockPyDecoder(None)
def closure(mode: str, *args) -> InfiniteMockPyDecoder:
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 = [
@ -1051,8 +1058,10 @@ class TestFileJpeg:
def test_repr_jpeg(self) -> None:
im = hopper()
b = im._repr_jpeg_()
assert b is not None
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
with Image.open(BytesIO(b)) as repr_jpeg:
assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17)

View File

@ -40,17 +40,17 @@ test_card.load()
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
def test_sanity() -> None:
# Internal version number
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000"))
version = features.version_codec("jpg_2000")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
with Image.open("Tests/images/test-card-lossless.jp2") as im:
px = im.load()
@ -77,7 +77,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
@ -289,6 +291,16 @@ def test_rgba(ext: str) -> None:
assert im.mode == "RGBA"
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
@skip_unless_feature_version("jpg_2000", "2.5.1")
def test_cmyk() -> None:
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as im:
assert im.mode == "CMYK"
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im:
@ -323,9 +335,15 @@ def test_issue_6194() -> None:
assert im.getpixel((5, 5)) == 31
def test_unknown_j2k_mode() -> None:
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/unknown_mode.j2k"):
pass
def test_unbound_local() -> None:
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError):
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/unbound_variable.jp2"):
pass
@ -340,6 +358,7 @@ def test_parser_feed() -> None:
p.feed(data)
# Assert
assert p.image is not None
assert p.image.size == (640, 480)
@ -363,6 +382,16 @@ def test_subsampling_decode(name: str) -> 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"
@ -435,3 +464,9 @@ def test_plt_marker() -> None:
hdr = out.read(2)
length = _binary.i16be(hdr)
out.seek(length - 2, os.SEEK_CUR)
def test_9bit() -> None:
with Image.open("Tests/images/9bit.j2k") as im:
assert im.mode == "I;16"
assert im.size == (128, 128)

View File

@ -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: Image.Image) -> 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"
@ -52,7 +52,9 @@ class LibTiffTestCase:
class TestFileLibTiff(LibTiffTestCase):
def test_version(self) -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff"))
version = features.version_codec("libtiff")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+t?$", version)
def test_g4_tiff(self, tmp_path: Path) -> None:
"""Test the ordinary file path load path"""
@ -90,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
with open(test_file, "rb") as f:
s.write(f.read())
s.seek(0)
r = io.BufferedReader(s)
data = f.read()
class NonBytesIO(io.RawIOBase):
def read(self, size: int = -1) -> bytes:
nonlocal data
if size == -1:
size = len(data)
result = data[:size]
data = data[size:]
return result
def readable(self) -> bool:
return True
r = io.BufferedReader(NonBytesIO())
with Image.open(r) as im:
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
@ -185,7 +198,9 @@ class TestFileLibTiff(LibTiffTestCase):
assert field in reloaded, f"{field} not in metadata"
@pytest.mark.valgrind_known_error(reason="Known invalid metadata")
def test_additional_metadata(self, tmp_path: Path) -> None:
def test_additional_metadata(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# these should not crash. Seriously dummy data, most of it doesn't make
# any sense, so we're running up against limits where we're asking
# libtiff to do stupid things.
@ -225,9 +240,10 @@ class TestFileLibTiff(LibTiffTestCase):
new_ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, info in core_items.items():
assert info.type is not None
if info.length == 1:
new_ifd[tag] = values[info.type]
if info.length == 0:
elif not info.length:
new_ifd[tag] = tuple(values[info.type] for _ in range(3))
else:
new_ifd[tag] = tuple(values[info.type] for _ in range(info.length))
@ -236,94 +252,109 @@ class TestFileLibTiff(LibTiffTestCase):
del new_ifd[338]
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, tiffinfo=new_ifd)
TiffImagePlugin.WRITE_LIBTIFF = False
@pytest.mark.parametrize(
"libtiff",
(
pytest.param(
True,
marks=pytest.mark.skipif(
not getattr(Image.core, "libtiff_support_custom_tags", False),
reason="Custom tags not supported by older libtiff",
),
),
False,
),
)
def test_custom_metadata(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
class Tc(NamedTuple):
value: Any
type: int
supported_by_default: bool
def test_custom_metadata(self, tmp_path: Path) -> None:
tc = namedtuple("tc", "value,type,supported_by_default")
custom = {
37000 + k: v
for k, v in enumerate(
[
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),
]
)
}
libtiffs = [False]
if Image.core.libtiff_support_custom_tags:
libtiffs.append(True)
def check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
) -> None:
im = hopper()
for libtiff in libtiffs:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
out = str(tmp_path / "temp.tif")
im.save(out, tiffinfo=tiffinfo)
def check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
) -> None:
im = hopper()
with Image.open(out) as reloaded:
for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag]
if (
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
and libtiff
):
# libtiff does not support real RATIONALS
assert round(abs(float(reloaded_value) - float(value)), 7) == 0
continue
out = str(tmp_path / "temp.tif")
im.save(out, tiffinfo=tiffinfo)
assert reloaded_value == value
with Image.open(out) as reloaded:
for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag]
if (
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
and libtiff
):
# libtiff does not support real RATIONALS
assert (
round(abs(float(reloaded_value) - float(value)), 7) == 0
)
continue
# Test with types
ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, tagdata in custom.items():
ifd[tag] = tagdata.value
ifd.tagtype[tag] = tagdata.type
check_tags(ifd)
assert reloaded_value == value
# Test without types. This only works for some types, int for example are
# always encoded as LONG and not SIGNED_LONG.
check_tags(
{
tag: tagdata.value
for tag, tagdata in custom.items()
if tagdata.supported_by_default
}
)
# Test with types
ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, tagdata in custom.items():
ifd[tag] = tagdata.value
ifd.tagtype[tag] = tagdata.type
check_tags(ifd)
# Test without types. This only works for some types, int for example are
# always encoded as LONG and not SIGNED_LONG.
check_tags(
{
tag: tagdata.value
for tag, tagdata in custom.items()
if tagdata.supported_by_default
}
)
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")
@ -333,24 +364,24 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault
im.save(outfile)
def test_xmlpacket_tag(self, tmp_path: Path) -> None:
TiffImagePlugin.WRITE_LIBTIFF = True
def test_xmlpacket_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = str(tmp_path / "temp.tif")
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded:
if 700 in reloaded.tag_v2:
assert reloaded.tag_v2[700] == b"xmlpacket tag"
def test_int_dpi(self, tmp_path: Path) -> None:
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
# issue #1765
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, dpi=(72, 72))
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded:
assert reloaded.info["dpi"] == (72.0, 72.0)
@ -412,13 +443,13 @@ class TestFileLibTiff(LibTiffTestCase):
assert "temp.tif" == reread.tag_v2[269]
assert "temp.tif" == reread.tag[269][0]
def test_12bit_rawmode(self) -> None:
def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Are we generating the same interpretation
of the image as Imagemagick is?"""
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/12bit.cropped.tif") as im:
im.load()
TiffImagePlugin.READ_LIBTIFF = False
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False)
# to make the target --
# convert 12bit.cropped.tif -depth 16 tmp.tif
# convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
@ -504,12 +535,13 @@ 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: Image.Image, tmp_path: Path) -> None:
def test_palette_save(
self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out)
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded:
# colormap/palette tag
@ -524,7 +556,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)
@ -537,9 +570,9 @@ class TestFileLibTiff(LibTiffTestCase):
with pytest.raises(OSError):
os.close(fn)
def test_multipage(self) -> None:
def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None:
# issue #862
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im:
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
@ -558,11 +591,9 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (20, 20)
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
TiffImagePlugin.READ_LIBTIFF = False
def test_multipage_nframes(self) -> None:
def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None:
# issue #862
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im:
frames = im.n_frames
assert frames == 3
@ -571,10 +602,8 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise ValueError: I/O operation on closed file
im.load()
TiffImagePlugin.READ_LIBTIFF = False
def test_multipage_seek_backwards(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im:
im.seek(1)
im.load()
@ -582,24 +611,21 @@ class TestFileLibTiff(LibTiffTestCase):
im.seek(0)
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
TiffImagePlugin.READ_LIBTIFF = False
def test__next(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/hopper.tif") as im:
assert not im.tag.next
im.load()
assert not im.tag.next
def test_4bit(self) -> None:
def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
test_file = "Tests/images/hopper_gray_4bpp.tif"
original = hopper("L")
# Act
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open(test_file) as im:
TiffImagePlugin.READ_LIBTIFF = False
# Assert
assert im.size == (128, 128)
@ -639,12 +665,12 @@ class TestFileLibTiff(LibTiffTestCase):
assert im2.mode == "L"
assert_image_equal(im, im2)
def test_save_bytesio(self) -> None:
def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None:
# PR 1011
# Test TIFF saving to io.BytesIO() object.
TiffImagePlugin.WRITE_LIBTIFF = True
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
# Generate test image
pilim = hopper()
@ -654,16 +680,14 @@ class TestFileLibTiff(LibTiffTestCase):
pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0)
assert_image_similar_tofile(pilim, buffer_io, 0)
with Image.open(buffer_io) as saved_im:
assert_image_similar(pilim, saved_im, 0)
save_bytesio()
save_bytesio("raw")
save_bytesio("packbits")
save_bytesio("tiff_lzw")
TiffImagePlugin.WRITE_LIBTIFF = False
TiffImagePlugin.READ_LIBTIFF = False
def test_save_ycbcr(self, tmp_path: Path) -> None:
im = hopper("YCbCr")
outfile = str(tmp_path / "temp.tif")
@ -673,25 +697,31 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
def test_exif_ifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
def test_exif_ifd(self) -> None:
out = io.BytesIO()
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert im.tag_v2[34665] == 125456
im.save(outfile)
im.save(out, "TIFF")
with Image.open(outfile) as reloaded:
with Image.open(out) as reloaded:
assert 34665 not in reloaded.tag_v2
im.save(out, "TIFF", tiffinfo={34665: 125456})
with Image.open(out) as reloaded:
if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456
def test_crashing_metadata(self, tmp_path: Path) -> None:
def test_crashing_metadata(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# issue 1597
with Image.open("Tests/images/rdf.tif") as im:
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
# this shouldn't crash
im.save(out, format="TIFF")
TiffImagePlugin.WRITE_LIBTIFF = False
def test_page_number_x_0(self, tmp_path: Path) -> None:
# Issue 973
@ -716,41 +746,47 @@ 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.
os.remove(tmpfile)
def test_read_icc(self) -> None:
def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc = img.info.get("icc_profile")
assert icc is not None
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_libtiff = img.info.get("icc_profile")
assert icc_libtiff is not None
TiffImagePlugin.READ_LIBTIFF = False
assert icc == icc_libtiff
def test_write_icc(self, tmp_path: Path) -> None:
def check_write(libtiff: bool) -> None:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
@pytest.mark.parametrize(
"libtiff",
(
pytest.param(
True,
marks=pytest.mark.skipif(
not getattr(Image.core, "libtiff_support_custom_tags", False),
reason="Custom tags not supported by older libtiff",
),
),
False,
),
)
def test_write_icc(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_profile = img.info["icc_profile"]
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_profile = img.info["icc_profile"]
out = str(tmp_path / "temp.tif")
img.save(out, icc_profile=icc_profile)
with Image.open(out) as reloaded:
assert icc_profile == reloaded.info["icc_profile"]
libtiffs = []
if Image.core.libtiff_support_custom_tags:
libtiffs.append(True)
libtiffs.append(False)
for libtiff in libtiffs:
check_write(libtiff)
out = str(tmp_path / "temp.tif")
img.save(out, icc_profile=icc_profile)
with Image.open(out) as reloaded:
assert icc_profile == reloaded.info["icc_profile"]
def test_multipage_compression(self) -> None:
with Image.open("Tests/images/compression.tif") as im:
@ -828,12 +864,13 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB")
def test_sampleformat_write(self, tmp_path: Path) -> None:
def test_sampleformat_write(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
im = Image.new("F", (1, 1))
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out)
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded:
assert reloaded.mode == "F"
@ -1023,7 +1060,11 @@ class TestFileLibTiff(LibTiffTestCase):
],
)
def test_wrong_bits_per_sample(
self, file_name: str, mode: str, size: tuple[int, int], tile
self,
file_name: str,
mode: str,
size: tuple[int, int],
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
) -> None:
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
@ -1079,15 +1120,14 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as im:
im.load()
def test_realloc_overflow(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e:
im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "-9"
TiffImagePlugin.READ_LIBTIFF = False
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
@ -1097,6 +1137,7 @@ class TestFileLibTiff(LibTiffTestCase):
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))
@ -1110,9 +1151,10 @@ class TestFileLibTiff(LibTiffTestCase):
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, **arguments)
im.save(out, "TIFF", **arguments)
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[STRIPOFFSETS]) == 1
finally:
TiffImagePlugin.STRIP_SIZE = 65536

View File

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

39
Tests/test_file_mpeg.py Normal file
View File

@ -0,0 +1,39 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image, MpegImagePlugin
def test_identify() -> None:
# Arrange
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
# Act
with Image.open(b) as im:
# Assert
assert im.format == "MPEG"
assert im.mode == "RGB"
assert im.size == (16, 1)
def test_invalid_file() -> None:
# Arrange
invalid_file = "Tests/images/flower.jpg"
# Act / Assert
with pytest.raises(SyntaxError):
MpegImagePlugin.MpegImageFile(invalid_file)
def test_load() -> None:
# Arrange
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
with Image.open(b) as im:
# Act / Assert: cannot load
with pytest.raises(OSError):
im.load()

View File

@ -6,7 +6,7 @@ from typing import Any
import pytest
from PIL import Image
from PIL import Image, ImageFile, MpoImagePlugin
from .helper import (
assert_image_equal,
@ -20,14 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg")
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
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 Image.open(out)
@pytest.mark.parametrize("test_file", test_files)
@ -88,7 +85,9 @@ def test_exif(test_file: str) -> None:
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
for im in (im_original, im_reloaded):
assert isinstance(im, MpoImagePlugin.MpoImageFile)
info = im._getexif()
assert info is not None
assert info[272] == "Nintendo 3DS"
assert info[296] == 2
assert info[34665] == 188
@ -96,7 +95,7 @@ def test_exif(test_file: str) -> 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)
@ -229,6 +228,17 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)
def test_ultra_hdr() -> None:
with Image.open("Tests/images/ultrahdr.jpg") as im:
assert im.format == "JPEG"
@pytest.mark.parametrize("test_file", test_files)
def test_image_grab(test_file: str) -> None:
with Image.open(test_file) as im:
@ -273,6 +283,8 @@ def test_save_all() -> None:
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1)

View File

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

View File

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

View File

@ -5,7 +5,9 @@ import os
import os.path
import tempfile
import time
from collections.abc import Generator
from pathlib import Path
from typing import Any
import pytest
@ -14,7 +16,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 +43,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,11 +114,11 @@ 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")
im.save(outfile, **params)
im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp:
contents = fp.read()
@ -156,7 +158,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 +228,8 @@ 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:
assert pdf.pages_ref is not None
pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info
assert b"Kids" in pages_info
@ -339,7 +342,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.

View File

@ -6,7 +6,8 @@ import warnings
import zlib
from io import BytesIO
from pathlib import Path
from typing import Any
from types import ModuleType
from typing import Any, cast
import pytest
@ -23,6 +24,7 @@ from .helper import (
skip_unless_feature,
)
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
@ -39,7 +41,7 @@ MAGIC = PngImagePlugin._MAGIC
def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data)
PngImagePlugin.putchunk(test_file, cid, *data)
return test_file.getvalue()
@ -57,11 +59,11 @@ def load(data: bytes) -> Image.Image:
return Image.open(BytesIO(data))
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
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")
@ -83,7 +85,9 @@ class TestFilePng:
def test_sanity(self, tmp_path: Path) -> None:
# internal version number
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
version = features.version_codec("zlib")
assert version is not None
assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version)
test_file = str(tmp_path / "temp.png")
@ -100,7 +104,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)
@ -302,8 +306,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
@ -531,8 +535,10 @@ class TestFilePng:
def test_repr_png(self) -> None:
im = hopper()
b = im._repr_png_()
assert b is not None
with Image.open(BytesIO(im._repr_png_())) as repr_png:
with Image.open(BytesIO(b)) as repr_png:
assert repr_png.format == "PNG"
assert_image_equal(im, repr_png)
@ -617,6 +623,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
@ -647,11 +657,12 @@ class TestFilePng:
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_specify_bits(self, tmp_path: Path) -> None:
@pytest.mark.parametrize("save_all", (True, False))
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
im = hopper("P")
out = str(tmp_path / "temp.png")
im.save(out, bits=4)
im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 48
@ -675,6 +686,7 @@ class TestFilePng:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
@ -759,14 +771,10 @@ class TestFilePng:
def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
if buffer:
class MyStdOut:
buffer = BytesIO()
class MyStdOut:
buffer = BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
@ -776,11 +784,23 @@ class TestFilePng:
# Reset stdout
sys.stdout = old_stdout
if buffer:
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
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")

View File

@ -88,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:
@ -241,13 +241,23 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> 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()
@ -358,14 +368,10 @@ def test_mimetypes(tmp_path: Path) -> None:
def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout
if buffer:
class MyStdOut:
buffer = BytesIO()
class MyStdOut:
buffer = BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
@ -375,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
# Reset stdout
sys.stdout = old_stdout
if buffer:
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_FILE)

View File

@ -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
@ -145,20 +150,26 @@ def test_combined_larger_than_size() -> None:
@pytest.mark.parametrize(
"test_file,raises",
[
(
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
Image.UnidentifiedImageError,
),
(
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
Image.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: type[Exception]) -> None:
with open(test_file, "rb") as f:
with pytest.raises(raises):
with Image.open(f):
pass
@pytest.mark.parametrize(
"test_file",
[
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
],
)
def test_layer_crashes(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(SyntaxError):
im.layers

View File

@ -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"
@ -105,6 +105,7 @@ def test_load_image_series() -> None:
img_list = SpiderImagePlugin.loadImageSeries(file_list)
# Assert
assert img_list is not None
assert len(img_list) == 1
assert isinstance(img_list[0], Image.Image)
assert img_list[0].size == (128, 128)
@ -160,4 +161,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)

View File

@ -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,14 +65,28 @@ 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")
assert im.palette.mode == "RGBA"
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
out = str(tmp_path / "temp.png")
im.save(out)
with Image.open(out) as reloaded:
assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png")
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
def test_rgba_16() -> None:
with Image.open("Tests/images/rgba16.tga") as im:
assert im.mode == "RGBA"
assert im.getpixel((0, 0)) == (172, 0, 255, 255)
assert im.getpixel((1, 0)) == (0, 255, 82, 0)
def test_id_field() -> None:
@ -133,6 +147,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")

View File

@ -2,8 +2,10 @@ from __future__ import annotations
import os
import warnings
from collections.abc import Generator
from io import BytesIO
from pathlib import Path
from types import ModuleType
import pytest
@ -20,6 +22,7 @@ from .helper import (
is_win32,
)
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
@ -75,6 +78,7 @@ class TestFileTiff:
def test_seek_after_close(self) -> None:
im = Image.open("Tests/images/multipage.tiff")
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.close()
with pytest.raises(ValueError):
@ -110,10 +114,14 @@ 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) -> None:
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:
ifd.legacy_api = None
ifd.legacy_api = False
assert str(e.value) == "Not allowing setting of legacy api"
def test_xyres_tiff(self) -> None:
@ -156,7 +164,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 +292,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 +410,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)
@ -417,13 +425,13 @@ class TestFileTiff:
def test_load_float(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdabcd"
ret = ifd.load_float(data, False)
ret = getattr(ifd, "load_float")(data, False)
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
def test_load_double(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdefghabcdefgh"
ret = ifd.load_double(data, False)
ret = getattr(ifd, "load_double")(data, False)
assert ret == (8.540883223036124e194, 8.540883223036124e194)
def test_ifd_tag_type(self) -> None:
@ -431,7 +439,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 +519,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})
@ -592,7 +600,7 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename, **kwargs)
hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im:
# legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72
@ -614,12 +622,29 @@ class TestFileTiff:
assert_image_equal_tofile(im, tmpfile)
def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/hopper.tif") as im:
im.load()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[33723] = 1
ifd.tagtype[33723] = 4
im.tag_v2 = ifd
im.save(outfile)
with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 33723 not in im.tag_v2
def test_rowsperstrip(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
im = hopper()
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 +685,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 +714,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()
@ -751,6 +776,7 @@ class TestFileTiff:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
@ -860,7 +886,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):

View File

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

View File

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

View File

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

View File

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

View File

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

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