Merge branch 'main' into progress

This commit is contained in:
Andrew Murray 2025-12-06 16:48:19 +11:00 committed by GitHub
commit 8c3c981b31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
239 changed files with 5137 additions and 2908 deletions

View File

@ -13,24 +13,21 @@ aptget_update()
return 1
fi
}
if [[ $(uname) != CYGWIN* ]]; then
aptget_update || aptget_update retry || aptget_update retry
fi
aptget_update || aptget_update retry || aptget_update retry
set -e
if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev nasm
fi
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev nasm
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install numpy
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
@ -40,36 +37,24 @@ python3 -m pip install pyroma
# fails on beta 3.14 and PyPy
python3 -m pip install --only-binary=:all: pyarrow || true
if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install numpy
# 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
# 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 == 3.9 ]]; then
# To match pyproject.toml
python3 -m pip install "setuptools>=77"
fi
# webp
pushd depends && ./install_webp.sh && popd
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
pushd depends && ./install_raqm.sh && popd
# libavif
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
else
cd depends && ./install_extra_test_images.sh && cd ..
# 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
# TODO Update condition when pyqt6 supports free-threading
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
fi
# webp
pushd depends && ./install_webp.sh && popd
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
pushd depends && sudo ./install_raqm.sh && popd
# libavif
pushd depends && sudo ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -1 +1 @@
cibuildwheel==3.0.0
cibuildwheel==3.3.0

View File

@ -1,10 +1,13 @@
mypy==1.16.1
mypy==1.19.0
arro3-compute
arro3-core
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
numpy
packaging
pyarrow-stubs
pybind11
pytest
sphinx
types-atheris

1
.github/mergify.yml vendored
View File

@ -8,7 +8,6 @@ pull_request_rules:
- status-success=Docker Test Successful
- status-success=Windows Test Successful
- status-success=MinGW
- status-success=Cygwin Test Successful
actions:
merge:
method: merge

View File

@ -44,13 +44,13 @@ jobs:
language: python
dry-run: false
- name: Upload New Crash
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts
- name: Upload Legacy Crash
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: steps.run.outcome == 'success'
with:
name: crash

View File

@ -32,12 +32,12 @@ jobs:
name: Docs
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.x"
cache: pip

View File

@ -20,7 +20,7 @@ jobs:
name: Lint
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
@ -33,7 +33,7 @@ jobs:
lint-pre-commit-
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.x"
cache: pip

View File

@ -2,9 +2,6 @@
set -e
if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven
fi
brew install \
aom \
dav1d \

View File

@ -22,7 +22,7 @@ jobs:
steps:
- name: "Check issues"
uses: actions/stale@v9
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action"

View File

@ -1,154 +0,0 @@
name: Test Cygwin
on:
push:
branches:
- "**"
paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- "docs/**"
- "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- "docs/**"
- "wheels/**"
workflow_dispatch:
permissions:
contents: read
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: [9]
timeout-minutes: 40
name: Python 3.${{ matrix.python-minor-version }}
steps:
- name: Fix line endings
run: |
git config --global core.autocrlf input
- name: Checkout Pillow
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Cygwin
uses: cygwin/cygwin-install-action@v5
with:
packages: >
gcc-g++
ghostscript
git
ImageMagick
jpeg
libfreetype-devel
libimagequant-devel
libjpeg-devel
liblapack-devel
liblcms2-devel
libopenjp2-devel
libraqm-devel
libtiff-devel
libwebp-devel
libxcb-devel
libxcb-xinerama0
make
netpbm
perl
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter
wget
xorg-server-extra
zlib-devel
- name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v4
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
- name: Select Python version
run: |
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 }}-${{ hashFiles('.ci/install.sh') }}
restore-keys: |
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
- name: Build system information
run: |
dash.exe -c "python3 .github/workflows/system-info.py"
- name: Install dependencies
run: |
bash.exe .ci/install.sh
- name: Build
shell: bash.exe -eo pipefail -o igncr "{0}"
run: |
.ci/build.sh
- name: Test
run: |
bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
- name: Prepare to upload errors
if: failure()
run: |
dash.exe -c "mkdir -p Tests/errors"
- name: Upload errors
uses: actions/upload-artifact@v4
if: failure()
with:
name: errors
path: Tests/errors
- name: After success
run: |
bash.exe .ci/after_success.sh
rm C:\cygwin\bin\bash.EXE
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
flags: GHA_Cygwin
name: Cygwin Python 3.${{ matrix.python-minor-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:
contents: none
needs: build
runs-on: ubuntu-latest
name: Cygwin Test Successful
steps:
- name: Success
run: echo Cygwin Test Successful

View File

@ -47,8 +47,10 @@ jobs:
centos-stream-10-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-41-amd64,
debian-13-trixie-x86,
debian-13-trixie-amd64,
fedora-42-amd64,
fedora-43-amd64,
gentoo,
ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64,
@ -66,7 +68,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false

View File

@ -45,7 +45,7 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false

View File

@ -41,7 +41,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false

View File

@ -39,7 +39,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false

View File

@ -35,11 +35,11 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"]
python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"]
architecture: ["x64"]
include:
# Test the oldest Python on 32-bit
- { python-version: "3.9", architecture: "x86" }
- { python-version: "3.10", architecture: "x86" }
timeout-minutes: 45
@ -47,19 +47,19 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Checkout cached dependencies
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
repository: python-pillow/pillow-depends
path: winbuild\depends
- name: Checkout extra test images
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
repository: python-pillow/test-images
@ -67,7 +67,7 @@ jobs:
# sets env: pythonLocation
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@ -97,8 +97,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.5.1 --no-progress
echo "C:\Program Files\gs\gs10.05.1\bin" >> $env:GITHUB_PATH
choco install ghostscript --version=10.6.0 --no-progress
echo "C:\Program Files\gs\gs10.06.0\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
@ -216,7 +216,7 @@ jobs:
shell: bash
- name: Upload errors
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: failure()
with:
name: errors

View File

@ -42,7 +42,6 @@ jobs:
]
python-version: [
"pypy3.11",
"pypy3.10",
"3.14t",
"3.14",
"3.13t",
@ -50,29 +49,28 @@ jobs:
"3.12",
"3.11",
"3.10",
"3.9",
]
include:
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.11", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
# M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" }
# Intel
- { os: "macos-15-intel", python-version: "3.10" }
exclude:
- { os: "macos-latest", python-version: "3.9" }
- { os: "macos-latest", python-version: "3.10" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@ -113,7 +111,7 @@ jobs:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'"
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build
@ -142,7 +140,7 @@ jobs:
mkdir -p Tests/errors
- name: Upload errors
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
if: failure()
with:
name: errors

View File

@ -32,7 +32,6 @@ if [[ "$CIBW_PLATFORM" == "ios" ]]; then
# or `build/deps/iphonesimulator`
WORKDIR=$(pwd)/build/$IOS_SDK
BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK
PATCH_DIR=$(pwd)/patches/iOS
# GNU tooling insists on using aarch64 rather than arm64
if [[ $PLAT == "arm64" ]]; then
@ -60,7 +59,7 @@ if [[ "$CIBW_PLATFORM" == "ios" ]]; then
# on using the Xcode builder, which isn't very helpful for most of Pillow's
# dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS
# etc. to ensure the right sysroot is selected.
HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO"
HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO -DENABLE_SHARED=NO"
# Meson needs to be pointed at a cross-platform configuration file
# This will be generated once CC etc. have been evaluated.
@ -90,23 +89,29 @@ fi
ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds. Version numbers with "Patched"
# annotations have a source code patch that is required for some platforms. If
# you change those versions, ensure the patch is also updated.
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.2.1
LIBPNG_VERSION=1.6.49
JPEGTURBO_VERSION=3.1.1
OPENJPEG_VERSION=2.5.3
# Package versions for fresh source builds.
if [[ -n "$IOS_SDK" ]]; then
FREETYPE_VERSION=2.13.3
else
FREETYPE_VERSION=2.14.1
fi
HARFBUZZ_VERSION=12.2.0
LIBPNG_VERSION=1.6.51
JPEGTURBO_VERSION=3.1.2
OPENJPEG_VERSION=2.5.4
XZ_VERSION=5.8.1
TIFF_VERSION=4.7.0
ZSTD_VERSION=1.5.7
TIFF_VERSION=4.7.1
LCMS2_VERSION=2.17
ZLIB_VERSION=1.3.1
ZLIB_NG_VERSION=2.2.4
LIBWEBP_VERSION=1.5.0 # Patched; next release won't need patching. See patch file.
if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "aarch64" ]]; then
ZLIB_NG_VERSION=2.2.5
else
ZLIB_NG_VERSION=2.3.1
fi
LIBWEBP_VERSION=1.6.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file.
BROTLI_VERSION=1.2.0
LIBAVIF_VERSION=1.3.0
function build_pkg_config {
@ -145,18 +150,13 @@ function build_zlib_ng {
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS
unset HOST_CONFIGURE_FLAGS
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat
if [[ "$ZLIB_NG_VERSION" == 2.2.5 ]]; then
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat
else
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat
fi
HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS
if [[ -n "$IS_MACOS" ]] && [[ -z "$IOS_SDK" ]]; then
# Ensure that on macOS, the library name is an absolute path, not an
# @rpath, so that delocate picks up the right library (and doesn't need
# DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an
# option to control the install_name. This isn't needed on iOS, as iOS
# only builds the static library.
install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
fi
touch zlib-stamp
}
@ -164,8 +164,8 @@ function build_brotli {
if [ -e brotli-stamp ]; then return; fi
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
&& make install)
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib -DCMAKE_MACOSX_BUNDLE=OFF $HOST_CMAKE_FLAGS . \
&& make -j4 install)
touch brotli-stamp
}
@ -186,30 +186,43 @@ function build_libavif {
python3 -m pip install meson ninja
if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then
if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IOS_SDK" ]]) || [ -n "$SANITIZER" ]; then
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
fi
local build_type=MinSizeRel
local build_shared=ON
local lto=ON
local libavif_cmake_flags
if [ -n "$IS_MACOS" ]; then
if [[ -n "$IS_MACOS" ]]; then
lto=OFF
libavif_cmake_flags=(
-DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \
)
if [[ -n "$IOS_SDK" ]]; then
build_shared=OFF
fi
else
if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then
build_type=Release
fi
libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now")
fi
if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then
libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic)
else
libavif_cmake_flags+=(
-DAVIF_CODEC_AOM_DECODE=OFF \
-DAVIF_CODEC_DAV1D=LOCAL
)
fi
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
# CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject
# of libavif) that disables support for encoding high bit depth images.
(cd $out_dir \
@ -217,30 +230,45 @@ function build_libavif {
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
-DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \
-DBUILD_SHARED_LIBS=ON \
-DBUILD_SHARED_LIBS=$build_shared \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_AOM=LOCAL \
-DCONFIG_AV1_HIGHBITDEPTH=0 \
-DAVIF_CODEC_AOM_DECODE=OFF \
-DAVIF_CODEC_DAV1D=LOCAL \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
-DCMAKE_C_VISIBILITY_PRESET=hidden \
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \
-DCMAKE_BUILD_TYPE=$build_type \
"${libavif_cmake_flags[@]}" \
. \
&& make install)
$HOST_CMAKE_FLAGS . )
if [[ -n "$IOS_SDK" ]]; then
# libavif's CMake configuration generates a meson cross file... but it
# doesn't work for iOS cross-compilation. Copy in Pillow-generated
# meson-cross config to replace the cmake-generated version.
cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson
fi
(cd $out_dir && make -j4 install)
touch libavif-stamp
}
function build_zstd {
if [ -e zstd-stamp ]; then return; fi
local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz)
(cd $out_dir \
&& make -j4 install)
touch zstd-stamp
}
function build {
build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel
fi
if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then
build_new_zlib
if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -headerpad_max_install_names" build_zlib_ng
else
build_zlib_ng
fi
@ -265,13 +293,11 @@ function build {
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
--disable-webp --disable-libdeflate --disable-zstd
else
build_zstd
build_tiff
fi
if [[ -z "$IOS_SDK" ]]; then
# Short term workaround; don't build libavif on iOS
build_libavif
fi
build_libavif
build_libpng
build_lcms2
build_openjpeg
@ -280,7 +306,11 @@ function build {
if [[ -n "$IS_MACOS" ]]; then
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
fi
CFLAGS="$CFLAGS $webp_cflags" build_simple libwebp $LIBWEBP_VERSION \
webp_ldflags=""
if [[ -n "$IOS_SDK" ]]; then
webp_ldflags="$webp_ldflags -llzma -lz"
fi
CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
--enable-libwebpmux --enable-libwebpdemux
@ -288,6 +318,10 @@ function build {
if [[ -n "$IS_MACOS" ]]; then
# Custom freetype build
if [[ -z "$IOS_SDK" ]]; then
build_simple sed 4.9 https://mirrors.middlendian.com/gnu/sed
fi
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
else
build_freetype
@ -380,6 +414,15 @@ fi
wrap_wheel_builder build
# A safety catch for iOS. iOS can't use dynamic libraries, but clang will prefer
# to link dynamic libraries to static libraries. The only way to reliably
# prevent this is to not have dynamic libraries available in the first place.
# The build process *shouldn't* generate any dylibs... but just in case, purge
# any dylibs that *have* been installed into the build prefix directory.
if [[ -n "$IOS_SDK" ]]; then
find "$BUILD_PREFIX" -name "*.dylib" -exec rm -rf {} \;
fi
# Return to the project root to finish the build
popd > /dev/null

View File

@ -39,6 +39,7 @@ concurrency:
cancel-in-progress: true
env:
EXPECTED_DISTS: 91
FORCE_COLOR: 1
jobs:
@ -52,21 +53,21 @@ jobs:
include:
- name: "macOS 10.10 x86_64"
platform: macos
os: macos-13
os: macos-15-intel
cibw_arch: x86_64
build: "cp3{9,10,11}*"
build: "cp3{10,11}*"
macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64"
platform: macos
os: macos-13
os: macos-15-intel
cibw_arch: x86_64
build: "cp3{12,13,14}*"
build: "cp3{12,13}*"
macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64"
platform: macos
os: macos-13
os: macos-15-intel
cibw_arch: x86_64
build: "pp3*"
build: "{cp314,pp3}*"
macosx_deployment_target: "10.15"
- name: "macOS arm64"
platform: macos
@ -77,22 +78,22 @@ jobs:
platform: linux
os: ubuntu-latest
cibw_arch: x86_64
manylinux: "manylinux2014"
- name: "manylinux_2_28 x86_64"
platform: linux
os: ubuntu-latest
cibw_arch: x86_64
build: "*manylinux*"
manylinux: "manylinux_2_28"
- name: "manylinux2014 and musllinux aarch64"
platform: linux
os: ubuntu-24.04-arm
cibw_arch: aarch64
manylinux: "manylinux2014"
- name: "manylinux_2_28 aarch64"
platform: linux
os: ubuntu-24.04-arm
cibw_arch: aarch64
build: "*manylinux*"
manylinux: "manylinux_2_28"
- name: "iOS arm64 device"
platform: ios
os: macos-latest
@ -103,15 +104,15 @@ jobs:
cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator"
platform: ios
os: macos-13
os: macos-15-intel
cibw_arch: x86_64_iphonesimulator
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: true
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.x"
@ -133,7 +134,7 @@ jobs:
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: dist-${{ matrix.name }}
path: ./wheelhouse/*.whl
@ -153,18 +154,18 @@ jobs:
- cibw_arch: ARM64
os: windows-11-arm
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Checkout extra test images
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
repository: python-pillow/test-images
path: Tests\test-images
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.x"
@ -219,46 +220,64 @@ jobs:
shell: cmd
- name: Upload wheels
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl
- name: Upload fribidi.dll
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: fribidi-windows-${{ matrix.cibw_arch }}
path: winbuild\build\bin\fribidi*
sdist:
if: github.event_name != 'schedule'
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.x"
- run: make sdist
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: dist-sdist
path: dist/*.tar.gz
count-dists:
needs: [build-native-wheels, windows, sdist]
runs-on: ubuntu-latest
name: Count dists
steps:
- uses: actions/download-artifact@v6
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: "What did we get?"
run: |
ls -alR
echo "Number of dists, should be $EXPECTED_DISTS:"
files=$(ls dist 2>/dev/null | wc -l)
echo $files
[ "$files" -eq $EXPECTED_DISTS ] || exit 1
scientific-python-nightly-wheels-publish:
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [build-native-wheels, windows]
needs: count-dists
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
with:
pattern: dist-*
pattern: dist-!(sdist)*
path: dist
merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels
@ -269,7 +288,7 @@ jobs:
pypi-publish:
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: [build-native-wheels, windows, sdist]
needs: count-dists
runs-on: ubuntu-latest
name: Upload release to PyPI
environment:
@ -278,7 +297,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
with:
pattern: dist-*
path: dist

4
.github/zizmor.yml vendored
View File

@ -1,6 +1,8 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/
# https://docs.zizmor.sh/configuration/
rules:
obfuscation:
disable: true
unpinned-uses:
config:
policies:

View File

@ -1,17 +1,17 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.2
rev: v0.14.7
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.1.0
rev: 25.11.0
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.8.6
rev: 1.9.2
hooks:
- id: bandit
args: [--severity-level=high]
@ -21,10 +21,10 @@ repos:
rev: v1.5.5
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.7
rev: v21.1.6
hooks:
- id: clang-format
types: [c]
@ -36,7 +36,7 @@ repos:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
@ -46,29 +46,29 @@ repos:
- id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer
exclude: ^Tests/images/|\.patch$
exclude: ^Tests/images/
- id: trailing-whitespace
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.2
rev: 0.35.0
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.11.0
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.18.0
hooks:
- id: zizmor
- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.0
rev: v1.0.2
hooks:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.6.0
rev: v2.11.1
hooks:
- id: pyproject-fmt
@ -79,7 +79,7 @@ repos:
additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.5.0
rev: 1.7.0
hooks:
- id: tox-ini-fmt

View File

@ -13,8 +13,8 @@ include LICENSE
include Makefile
include tox.ini
graft Tests
graft Tests/images
graft checks
graft patches
graft src
graft depends
graft winbuild
@ -28,8 +28,19 @@ exclude .editorconfig
exclude .readthedocs.yml
exclude codecov.yml
exclude renovate.json
exclude Tests/images/README.md
exclude Tests/images/crash*.tif
exclude Tests/images/string_dimension.tiff
global-exclude .git*
global-exclude *.pyc
global-exclude *.so
prune .ci
prune wheels
prune winbuild/build
prune winbuild/depends
prune Tests/errors
prune Tests/images/jpeg2000
prune Tests/images/msp
prune Tests/images/picins
prune Tests/images/sunraster
prune Tests/test-images

View File

@ -36,9 +36,6 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
alt="GitHub Actions build status (Test MinGW)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml"><img
alt="GitHub Actions build status (Test Cygwin)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>

1
Tests/createfontdatachunk.py Executable file → Normal file
View File

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

View File

@ -10,17 +10,20 @@ import shutil
import subprocess
import sys
import tempfile
from collections.abc import Sequence
from functools import lru_cache
from io import BytesIO
from pathlib import Path
from typing import Any, Callable
import pytest
from packaging.version import parse as parse_version
from PIL import Image, ImageFile, ImageMath, features
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
uploader = None
@ -172,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
return pytest.mark.skipif(not features.check(feature), reason=reason)
def has_feature_version(feature: str, required: str) -> bool:
version = features.version(feature)
assert version is not None
version_required = parse_version(required)
version_available = parse_version(version)
return version_available >= version_required
def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator:
@ -291,16 +302,6 @@ def djpeg_available() -> bool:
return False
def cjpeg_available() -> bool:
if shutil.which("cjpeg"):
try:
subprocess.check_call(["cjpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
return False
def netpbm_available() -> bool:
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
Tests/images/frame_size.mpo Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Binary file not shown.

275
Tests/test_arro3.py Normal file
View File

@ -0,0 +1,275 @@
from __future__ import annotations
import json
from typing import Any, NamedTuple
import pytest
from PIL import Image
from .helper import (
assert_deep_equal,
assert_image_equal,
hopper,
is_big_endian,
)
TYPE_CHECKING = False
if TYPE_CHECKING:
from arro3 import compute
from arro3.core import (
Array,
DataType,
Field,
fixed_size_list_array,
)
else:
arro3 = pytest.importorskip("arro3", reason="Arro3 not installed")
from arro3 import compute
from arro3.core import Array, DataType, Field, fixed_size_list_array
TEST_IMAGE_SIZE = (10, 10)
def _test_img_equals_pyarray(
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
) -> None:
assert img.height * img.width * elts_per_pixel == len(arr)
px = img.load()
assert px is not None
if elts_per_pixel > 1 and mask is None:
# have to do element-wise comparison when we're comparing
# flattened r,g,b,a to a pixel.
mask = list(range(elts_per_pixel))
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
if mask:
pixel = px[x, y]
assert isinstance(pixel, tuple)
for ix, elt in enumerate(mask):
if elts_per_pixel == 1:
assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
else:
assert (
pixel[ix]
== arr[(y * img.width + x) * elts_per_pixel + elt].as_py()
)
else:
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
def _test_img_equals_int32_pyarray(
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
) -> None:
assert img.height * img.width * elts_per_pixel == len(arr)
px = img.load()
assert px is not None
if mask is None:
# have to do element-wise comparison when we're comparing
# flattened rgba in an uint32 to a pixel.
mask = list(range(elts_per_pixel))
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
pixel = px[x, y]
assert isinstance(pixel, tuple)
arr_pixel_int = arr[y * img.width + x].as_py()
arr_pixel_tuple = (
arr_pixel_int % 256,
(arr_pixel_int // 256) % 256,
(arr_pixel_int // 256**2) % 256,
(arr_pixel_int // 256**3),
)
if is_big_endian():
arr_pixel_tuple = arr_pixel_tuple[::-1]
for ix, elt in enumerate(mask):
assert pixel[ix] == arr_pixel_tuple[elt]
fl_uint8_4_type = DataType.list(Field("_", DataType.uint8()).with_nullable(False), 4)
@pytest.mark.parametrize(
"mode, dtype, mask",
(
("L", DataType.uint8(), None),
("I", DataType.int32(), None),
("F", DataType.float32(), None),
("LA", fl_uint8_4_type, [0, 3]),
("RGB", fl_uint8_4_type, [0, 1, 2]),
("RGBA", fl_uint8_4_type, None),
("RGBX", fl_uint8_4_type, None),
("CMYK", fl_uint8_4_type, None),
("YCbCr", fl_uint8_4_type, [0, 1, 2]),
("HSV", fl_uint8_4_type, [0, 1, 2]),
),
)
def test_to_array(mode: str, dtype: DataType, mask: list[int] | None) -> None:
img = hopper(mode)
# Resize to non-square
img = img.crop((3, 0, 124, 127))
assert img.size == (121, 127)
arr = Array(img)
_test_img_equals_pyarray(img, arr, mask)
assert arr.type == dtype
reloaded = Image.fromarrow(arr, mode, img.size)
assert_image_equal(img, reloaded)
def test_lifetime() -> None:
# valgrind shouldn't error out here.
# arrays should be accessible after the image is deleted.
img = hopper("L")
arr_1 = Array(img)
arr_2 = Array(img)
del img
assert compute.sum(arr_1).as_py() > 0
del arr_1
assert compute.sum(arr_2).as_py() > 0
del arr_2
def test_lifetime2() -> None:
# valgrind shouldn't error out here.
# img should remain after the arrays are collected.
img = hopper("L")
arr_1 = Array(img)
arr_2 = Array(img)
assert compute.sum(arr_1).as_py() > 0
del arr_1
assert compute.sum(arr_2).as_py() > 0
del arr_2
img2 = img.copy()
px = img2.load()
assert px # make mypy happy
assert isinstance(px[0, 0], int)
class DataShape(NamedTuple):
dtype: DataType
# Strictly speaking, elt should be a pixel or pixel component, so
# list[uint8][4], float, int, uint32, uint8, etc. But more
# correctly, it should be exactly the dtype from the line above.
elt: Any
elts_per_pixel: int
UINT_ARR = DataShape(
dtype=fl_uint8_4_type,
elt=[1, 2, 3, 4], # array of 4 uint8 per pixel
elts_per_pixel=1, # only one array per pixel
)
UINT = DataShape(
dtype=DataType.uint8(),
elt=3, # one uint8,
elts_per_pixel=4, # but repeated 4x per pixel
)
UINT32 = DataShape(
dtype=DataType.uint32(),
elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000
elts_per_pixel=1, # one per pixel
)
INT32 = DataShape(
dtype=DataType.uint32(),
elt=0x12CDEF45, # one packed int
elts_per_pixel=1, # one per pixel
)
@pytest.mark.parametrize(
"mode, data_tp, mask",
(
("L", DataShape(DataType.uint8(), 3, 1), None),
("I", DataShape(DataType.int32(), 1 << 24, 1), None),
("F", DataShape(DataType.float32(), 3.14159, 1), None),
("LA", UINT_ARR, [0, 3]),
("LA", UINT, [0, 3]),
("RGB", UINT_ARR, [0, 1, 2]),
("RGBA", UINT_ARR, None),
("CMYK", UINT_ARR, None),
("YCbCr", UINT_ARR, [0, 1, 2]),
("HSV", UINT_ARR, [0, 1, 2]),
("RGB", UINT, [0, 1, 2]),
("RGBA", UINT, None),
("CMYK", UINT, None),
("YCbCr", UINT, [0, 1, 2]),
("HSV", UINT, [0, 1, 2]),
),
)
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
if dtype == fl_uint8_4_type:
tmp_arr = Array(elt * (ct_pixels * elts_per_pixel), type=DataType.uint8())
arr = fixed_size_list_array(tmp_arr, 4)
else:
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
_test_img_equals_pyarray(img, arr, mask, elts_per_pixel)
@pytest.mark.parametrize(
"mode, mask",
(
("LA", [0, 3]),
("RGB", [0, 1, 2]),
("RGBA", None),
("CMYK", None),
("YCbCr", [0, 1, 2]),
("HSV", [0, 1, 2]),
),
)
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
(dtype, elt, elts_per_pixel) = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
_test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)
@pytest.mark.parametrize(
"mode, metadata",
(
("LA", ["L", "X", "X", "A"]),
("RGB", ["R", "G", "B", "X"]),
("RGBX", ["R", "G", "B", "X"]),
("RGBA", ["R", "G", "B", "A"]),
("CMYK", ["C", "M", "Y", "K"]),
("YCbCr", ["Y", "Cb", "Cr", "X"]),
("HSV", ["H", "S", "V", "X"]),
),
)
def test_image_metadata(mode: str, metadata: list[str]) -> None:
img = hopper(mode)
arr = Array(img)
assert arr.type.value_field
assert arr.type.value_field.metadata
assert arr.type.value_field.metadata[b"image"]
parsed_metadata = json.loads(arr.type.value_field.metadata[b"image"].decode("utf8"))
assert "bands" in parsed_metadata
assert parsed_metadata["bands"] == metadata

View File

@ -2,7 +2,6 @@ from __future__ import annotations
import io
import re
from typing import Callable
import pytest
@ -10,6 +9,10 @@ from PIL import features
from .helper import skip_unless_feature
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
def test_check() -> None:
# Check the correctness of the convenience function
@ -18,11 +21,7 @@ def test_check() -> None:
for codec in features.codecs:
assert features.check_codec(codec) == features.check(codec)
for feature in features.features:
if "webp" in feature:
with pytest.warns(DeprecationWarning, match="webp"):
assert features.check_feature(feature) == features.check(feature)
else:
assert features.check_feature(feature) == features.check(feature)
assert features.check_feature(feature) == features.check(feature)
def test_version() -> None:
@ -48,11 +47,7 @@ def test_version() -> None:
for codec in features.codecs:
test(codec, features.version_codec)
for feature in features.features:
if "webp" in feature:
with pytest.warns(DeprecationWarning, match="webp"):
test(feature, features.version_feature)
else:
test(feature, features.version_feature)
test(feature, features.version_feature)
@skip_unless_feature("libjpeg_turbo")
@ -112,6 +107,25 @@ def test_unsupported_module() -> None:
features.version_module(module)
def test_unsupported_feature() -> None:
# Arrange
feature = "unsupported_feature"
# Act / Assert
with pytest.raises(ValueError):
features.check_feature(feature)
with pytest.raises(ValueError):
features.version_feature(feature)
def test_unsupported_version() -> None:
assert features.version("unsupported_version") is None
def test_modulenotfound(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(features, "features", {"test": ("PIL._test", "", "")})
assert features.check_feature("test") is None
@pytest.mark.parametrize("supported_formats", (True, False))
def test_pilinfo(supported_formats: bool) -> None:
buf = io.StringIO()

View File

@ -770,6 +770,25 @@ def test_apng_save_size(tmp_path: Path) -> None:
assert reloaded.size == (200, 200)
def test_compress_level() -> None:
compress_level_sizes = {}
for compress_level in (0, 9):
out = BytesIO()
im = Image.new("L", (100, 100))
im.save(
out,
"PNG",
save_all=True,
append_images=[Image.new("L", (200, 200))],
compress_level=compress_level,
)
compress_level_sizes[compress_level] = len(out.getvalue())
assert compress_level_sizes[0] > compress_level_sizes[9]
def test_seek_after_close() -> None:
im = Image.open("Tests/images/apng/delay.png")
im.seek(1)

View File

@ -14,6 +14,7 @@ import pytest
from PIL import (
AvifImagePlugin,
GifImagePlugin,
Image,
ImageDraw,
ImageFile,
@ -266,6 +267,7 @@ class TestFileAvif:
def test_background_from_gif(self, tmp_path: Path) -> None:
with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1))
assert isinstance(original_value, tuple)
# Save as AVIF
out_avif = tmp_path / "temp.avif"
@ -278,6 +280,7 @@ class TestFileAvif:
with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1))
assert isinstance(reread_value, tuple)
difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)])
assert difference <= 6
@ -286,6 +289,7 @@ class TestFileAvif:
with Image.open("Tests/images/chi.gif") as im:
im.save(temp_file)
with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 1
def test_invalid_file(self) -> None:
@ -644,10 +648,12 @@ class TestAvifAnimation:
"""
with Image.open(TEST_AVIF_FILE) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 1
assert not im.is_animated
with Image.open("Tests/images/avif/star.avifs") as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 5
assert im.is_animated
@ -658,11 +664,13 @@ class TestAvifAnimation:
"""
with Image.open("Tests/images/avif/star.gif") as original:
assert isinstance(original, GifImagePlugin.GifImageFile)
assert original.n_frames > 1
temp_file = tmp_path / "temp.avif"
original.save(temp_file, save_all=True)
with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == original.n_frames
# Compare first frame in P mode to frame from original GIF
@ -682,6 +690,7 @@ class TestAvifAnimation:
def check(temp_file: Path) -> None:
with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 4
# Compare first frame to original
@ -754,6 +763,7 @@ class TestAvifAnimation:
)
with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 5
assert im.is_animated
@ -783,6 +793,7 @@ class TestAvifAnimation:
)
with Image.open(temp_file) as im:
assert isinstance(im, AvifImagePlugin.AvifImageFile)
assert im.n_frames == 5
assert im.is_animated

View File

@ -6,6 +6,8 @@ from pathlib import Path
import pytest
from PIL import BmpImagePlugin, Image, _binary
from PIL._binary import o16le as o16
from PIL._binary import o32le as o32
from .helper import (
assert_image_equal,
@ -114,7 +116,7 @@ def test_save_float_dpi(tmp_path: Path) -> None:
def test_load_dib() -> None:
# test for #1293, Imagegrab returning Unsupported Bitfields Format
# test for #1293, ImageGrab returning Unsupported Bitfields Format
with Image.open("Tests/images/clipboard.dib") as im:
assert im.format == "DIB"
assert im.get_format_mimetype() == "image/bmp"
@ -219,6 +221,18 @@ def test_rle8_eof(file_name: str, length: int) -> None:
im.load()
def test_unsupported_bmp_bitfields_layout() -> None:
fp = io.BytesIO(
o32(40) # header size
+ b"\x00" * 10
+ o16(1) # bits
+ o32(3) # BITFIELDS compression
+ b"\x00" * 32
)
with pytest.raises(OSError, match="Unsupported BMP bitfields layout"):
Image.open(fp)
def test_offset() -> None:
# This image has been hexedited
# to exclude the palette size from the pixel data offset

View File

@ -1,8 +1,13 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import CurImagePlugin, Image
from PIL._binary import o8
from PIL._binary import o16le as o16
from PIL._binary import o32le as o32
TEST_FILE = "Tests/images/deerstalker.cur"
@ -17,6 +22,24 @@ def test_sanity() -> None:
assert im.getpixel((16, 16)) == (84, 87, 86, 255)
def test_largest_cursor() -> None:
magic = b"\x00\x00\x02\x00"
sizes = ((1, 1), (8, 8), (4, 4))
data = magic + o16(len(sizes))
for w, h in sizes:
image_offset = 6 + len(sizes) * 16 if (w, h) == max(sizes) else 0
data += o8(w) + o8(h) + o8(0) * 10 + o32(image_offset)
data += (
o32(12) # header size
+ o16(8) # width
+ o16(16) # height
+ o16(0) # planes
+ o16(1) # bits
)
with Image.open(BytesIO(data)) as im:
assert im.size == (8, 8)
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
@ -26,6 +49,7 @@ def test_invalid_file() -> None:
no_cursors_file = "Tests/images/no_cursors.cur"
cur = CurImagePlugin.CurImageFile(TEST_FILE)
assert cur.fp is not None
cur.fp.close()
with open(no_cursors_file, "rb") as cur.fp:
with pytest.raises(TypeError):

View File

@ -380,21 +380,33 @@ def test_palette() -> None:
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
def test_zero_mask_totals() -> None:
with Image.open("Tests/images/zero_mask_totals.dds") as im:
im.load()
def test_unsupported_header_size() -> None:
with pytest.raises(OSError, match="Unsupported header size 0"):
with Image.open(BytesIO(b"DDS " + b"\x00" * 4)):
pass
def test_unsupported_bitcount() -> None:
with pytest.raises(OSError):
with pytest.raises(OSError, match="Unsupported bitcount 24 for 131072"):
with Image.open("Tests/images/unsupported_bitcount.dds"):
pass
@pytest.mark.parametrize(
"test_file",
"test_file, message",
(
"Tests/images/unimplemented_dxgi_format.dds",
"Tests/images/unimplemented_pfflags.dds",
("Tests/images/unimplemented_dxgi_format.dds", "Unimplemented DXGI format 93"),
("Tests/images/unimplemented_pixel_format.dds", "Unimplemented pixel format 0"),
("Tests/images/unimplemented_pfflags.dds", "Unknown pixel format flags 8"),
),
)
def test_not_implemented(test_file: str) -> None:
with pytest.raises(NotImplementedError):
def test_not_implemented(test_file: str, message: str) -> None:
with pytest.raises(NotImplementedError, match=message):
with Image.open(test_file):
pass

View File

@ -197,6 +197,14 @@ def test_load_long_binary_data(prefix: bytes) -> None:
assert img.format == "EPS"
def test_begin_binary() -> None:
with open("Tests/images/eps/binary_preview_map.eps", "rb") as fp:
data = bytearray(fp.read())
data[76875 : 76875 + 11] = b"%" * 11
with Image.open(io.BytesIO(data)) as img:
assert img.size == (399, 480)
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)

View File

@ -48,6 +48,7 @@ def test_sanity() -> None:
def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open(animated_test_file_with_prefix_chunk) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.mode == "P"
assert im.size == (320, 200)
assert im.format == "FLI"
@ -55,6 +56,7 @@ def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
assert im.is_animated
palette = im.getpalette()
assert palette is not None
assert palette[3:6] == [255, 255, 255]
assert palette[381:384] == [204, 204, 12]
assert palette[765:] == [252, 0, 0]

View File

@ -1,8 +1,10 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import GbrImagePlugin, Image
from PIL import GbrImagePlugin, Image, _binary
from .helper import assert_image_equal_tofile
@ -31,8 +33,49 @@ def test_multiple_load_operations() -> None:
assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
def create_gbr_image(info: dict[str, int] = {}, magic_number: bytes = b"") -> BytesIO:
return BytesIO(
b"".join(
_binary.o32be(i)
for i in [
info.get("header_size", 20),
info.get("version", 1),
info.get("width", 1),
info.get("height", 1),
info.get("color_depth", 1),
]
)
+ magic_number
)
with pytest.raises(SyntaxError):
def test_invalid_file() -> None:
for f in [
create_gbr_image({"header_size": 0}),
create_gbr_image({"width": 0}),
create_gbr_image({"height": 0}),
]:
with pytest.raises(SyntaxError, match="not a GIMP brush"):
GbrImagePlugin.GbrImageFile(f)
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError, match="Unsupported GIMP brush version"):
GbrImagePlugin.GbrImageFile(invalid_file)
def test_unsupported_gimp_brush() -> None:
f = create_gbr_image({"color_depth": 2})
with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"):
GbrImagePlugin.GbrImageFile(f)
def test_bad_magic_number() -> None:
f = create_gbr_image({"version": 2}, magic_number=b"badm")
with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"):
GbrImagePlugin.GbrImageFile(f)
def test_L() -> None:
f = create_gbr_image()
with Image.open(f) as im:
assert im.mode == "L"

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import GdImageFile, UnidentifiedImageError
@ -16,6 +18,14 @@ def test_sanity() -> None:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14)
def test_transparency() -> None:
with open(TEST_GD_FILE, "rb") as fp:
data = bytearray(fp.read())
data[7:11] = b"\x00\x00\x00\x05"
with GdImageFile.open(BytesIO(data)) as im:
assert im.info["transparency"] == 5
def test_bad_mode() -> None:
with pytest.raises(ValueError):
GdImageFile.open(TEST_GD_FILE, "bad mode")

View File

@ -294,6 +294,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
im.save(out, save_all=True)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 5
@ -1421,6 +1422,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
with Image.open(out) as im:
# Assert that the frames are correct, and each frame has the same palette
assert isinstance(im, GifImagePlugin.GifImageFile)
assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
assert im.palette is not None
assert im.global_palette is not None

View File

@ -2,6 +2,8 @@ from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
from .helper import assert_image_equal, hopper
@ -9,21 +11,78 @@ from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/iptc.jpg"
def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
def field(tag: tuple[int, int], value: bytes) -> bytes:
return bytes((0x1C,) + tag + (0, len(value))) + value
data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
data += field((3, 120), bytes((info.get("compression", 1),)))
if "band" in info:
data += field((3, 65), bytes((info["band"] + 1,)))
data += field((3, 20), b"\x01") # width
data += field((3, 30), b"\x01") # height
data += field(
(8, 10),
bytes((info.get("data", 0),)),
)
return BytesIO(data)
def test_open() -> None:
expected = Image.new("L", (1, 1))
f = BytesIO(
b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
)
f = create_iptc_image()
with Image.open(f) as im:
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
assert im.tile == [("iptc", (0, 0, 1, 1), 25, ("raw", None))]
assert_image_equal(im, expected)
with Image.open(f) as im:
assert im.load() is not None
def test_field_length() -> None:
f = create_iptc_image()
f.seek(28)
f.write(b"\xff")
with pytest.raises(OSError, match="illegal field length in IPTC/NAA file"):
with Image.open(f):
pass
@pytest.mark.parametrize("layers, mode", ((3, "RGB"), (4, "CMYK")))
def test_layers(layers: int, mode: str) -> None:
for band in range(-1, layers):
info = {"layers": layers, "component": 1, "data": 5}
if band != -1:
info["band"] = band
f = create_iptc_image(info)
with Image.open(f) as im:
assert im.mode == mode
data = [0] * layers
data[max(band, 0)] = 5
assert im.getpixel((0, 0)) == tuple(data)
def test_unknown_compression() -> None:
f = create_iptc_image({"compression": 2})
with pytest.raises(OSError, match="Unknown IPTC image compression"):
with Image.open(f):
pass
def test_getiptcinfo() -> None:
f = create_iptc_image()
with Image.open(f) as im:
assert IptcImagePlugin.getiptcinfo(im) == {
(3, 60): b"\x01\x00",
(3, 120): b"\x01",
(3, 20): b"\x01",
(3, 30): b"\x01",
}
def test_getiptcinfo_jpg_none() -> None:
# Arrange
with hopper() as im:

View File

@ -26,7 +26,6 @@ from .helper import (
assert_image_equal_tofile,
assert_image_similar,
assert_image_similar_tofile,
cjpeg_available,
djpeg_available,
hopper,
is_win32,
@ -331,8 +330,10 @@ class TestFileJpeg:
# Reading
with Image.open("Tests/images/exif_gps.jpg") as im:
exif = im._getexif()
assert exif[gps_index] == expected_exif_gps
assert isinstance(im, JpegImagePlugin.JpegImageFile)
exif_data = im._getexif()
assert exif_data is not None
assert exif_data[gps_index] == expected_exif_gps
# Writing
f = tmp_path / "temp.jpg"
@ -341,8 +342,10 @@ class TestFileJpeg:
hopper().save(f, exif=exif)
with Image.open(f) as reloaded:
exif = reloaded._getexif()
assert exif[gps_index] == expected_exif_gps
assert isinstance(reloaded, JpegImagePlugin.JpegImageFile)
exif_data = reloaded._getexif()
assert exif_data is not None
assert exif_data[gps_index] == expected_exif_gps
def test_empty_exif_gps(self) -> None:
with Image.open("Tests/images/empty_gps_ifd.jpg") as im:
@ -369,6 +372,7 @@ class TestFileJpeg:
exifs = []
for i in range(2):
with Image.open("Tests/images/exif-200dpcm.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
exifs.append(im._getexif())
assert exifs[0] == exifs[1]
@ -402,13 +406,17 @@ class TestFileJpeg:
}
with Image.open("Tests/images/exif_gps.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
exif = im._getexif()
assert exif is not None
for tag, value in expected_exif.items():
assert value == exif[tag]
def test_exif_gps_typeerror(self) -> None:
with Image.open("Tests/images/exif_gps_typeerror.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
# Should not raise a TypeError
im._getexif()
@ -488,7 +496,9 @@ class TestFileJpeg:
def test_exif(self) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
info = im._getexif()
assert info is not None
assert info[305] == "Adobe Photoshop CS Macintosh"
def test_get_child_images(self) -> None:
@ -691,11 +701,13 @@ class TestFileJpeg:
def test_save_multiple_16bit_qtables(self) -> None:
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
im2 = self.roundtrip(im, qtables="keep")
assert im.quantization == im2.quantization
def test_save_single_16bit_qtable(self) -> None:
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
im2 = self.roundtrip(im, qtables={0: im.quantization[0]})
assert len(im2.quantization) == 1
assert im2.quantization[0] == im.quantization[0]
@ -731,14 +743,6 @@ class TestFileJpeg:
img.load_djpeg()
assert_image_similar_tofile(img, TEST_FILE, 5)
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
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, BytesIO(), tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17)
def test_no_duplicate_0x1001_tag(self) -> None:
# Arrange
tag_ids = {v: k for k, v in ExifTags.TAGS.items()}
@ -907,7 +911,10 @@ class TestFileJpeg:
# in contrast to normal 8
with Image.open("Tests/images/exif-ifd-offset.jpg") as im:
# Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09"
assert isinstance(im, JpegImagePlugin.JpegImageFile)
exif = im._getexif()
assert exif is not None
assert exif[306] == "2017:03:13 23:03:09"
def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im:

View File

@ -355,6 +355,56 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault
im.save(outfile)
@pytest.mark.parametrize("tagtype", (TiffTags.SIGNED_RATIONAL, TiffTags.IFD))
def test_tag_type(
self, tagtype: int, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[37000] = 100
ifd.tagtype[37000] = tagtype
out = tmp_path / "temp.tif"
im = Image.new("L", (1, 1))
im.save(out, tiffinfo=ifd)
with Image.open(out) as reloaded:
assert reloaded.tag_v2[37000] == 100
def test_inknames_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = tmp_path / "temp.tif"
hopper("L").save(out, tiffinfo={333: "name\x00"})
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[333] in ("name", "name\x00")
def test_whitepoint_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = tmp_path / "temp.tif"
hopper().save(out, tiffinfo={318: (0.3127, 0.3289)})
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289))
# Save tag by default
out = tmp_path / "temp2.tif"
with Image.open("Tests/images/rdf.tif") as im:
im.save(out)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289999))
def test_xmlpacket_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
@ -365,8 +415,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
if 700 in reloaded.tag_v2:
assert reloaded.tag_v2[700] == b"xmlpacket tag"
assert reloaded.tag_v2[700] == b"xmlpacket tag"
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
# issue #1765
@ -873,8 +922,8 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.mode == "RGB"
assert im.size == (128, 128)
assert im.format == "TIFF"
im2 = hopper()
assert_image_similar(im, im2, 5)
with hopper() as im2:
assert_image_similar(im, im2, 5)
except OSError:
captured = capfd.readouterr()
if "LZMA compression support is not configured" in captured.err:

View File

@ -22,10 +22,10 @@ def test_sanity() -> None:
# Adjust for the gamma of 2.2 encoded into the file
lut = ImagePalette.make_gamma_lut(1 / 2.2)
im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()])
im1 = Image.merge("RGBA", [chan.point(lut) for chan in im.split()])
im2 = hopper("RGBA")
assert_image_similar(im, im2, 10)
assert_image_similar(im1, im2, 10)
def test_n_frames() -> None:

View File

@ -104,25 +104,27 @@ def test_exif(test_file: str) -> None:
def test_frame_size() -> None:
# This image has been hexedited to contain a different size
# in the SOF marker of the second frame
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
assert im.size == (640, 480)
with Image.open("Tests/images/frame_size.mpo") as im:
assert im.size == (56, 70)
im.load()
im.seek(1)
assert im.size == (680, 480)
assert im.size == (349, 434)
im.load()
im.seek(0)
assert im.size == (640, 480)
assert im.size == (56, 70)
def test_ignore_frame_size() -> None:
# Ignore the different size of the second frame
# since this is not a "Large Thumbnail" image
with Image.open("Tests/images/ignore_frame_size.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.size == (64, 64)
im.seek(1)
assert im.mpinfo is not None
assert (
im.mpinfo[0xB002][1]["Attribute"]["MPType"]
== "Multi-Frame Image: (Disparity)"
@ -155,6 +157,7 @@ def test_reload_exif_after_seek() -> None:
@pytest.mark.parametrize("test_file", test_files)
def test_mp(test_file: str) -> None:
with Image.open(test_file) as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
mpinfo = im._getmp()
assert mpinfo is not None
assert mpinfo[45056] == b"0100"
@ -165,6 +168,7 @@ def test_mp_offset() -> None:
# This image has been manually hexedited to have an IFD offset of 10
# in APP2 data, in contrast to normal 8
with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
mpinfo = im._getmp()
assert mpinfo is not None
assert mpinfo[45056] == b"0100"
@ -182,6 +186,7 @@ def test_mp_no_data() -> None:
@pytest.mark.parametrize("test_file", test_files)
def test_mp_attribute(test_file: str) -> None:
with Image.open(test_file) as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
mpinfo = im._getmp()
assert mpinfo is not None
for frame_number, mpentry in enumerate(mpinfo[0xB002]):
@ -295,12 +300,12 @@ def test_save_all() -> None:
im_reloaded.seek(1)
assert_image_similar(im, im_reloaded, 30)
im = Image.new("RGB", (1, 1))
im_rgb = Image.new("RGB", (1, 1))
for colors in (("#f00",), ("#f00", "#0f0")):
append_images = [Image.new("RGB", (1, 1), color) for color in colors]
im_reloaded = roundtrip(im, save_all=True, append_images=append_images)
im_reloaded = roundtrip(im_rgb, save_all=True, append_images=append_images)
assert_image_equal(im, im_reloaded)
assert_image_equal(im_rgb, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100"
@ -310,7 +315,7 @@ def test_save_all() -> None:
assert_image_similar(im_reloaded, im_expected, 1)
# Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im, save_all=True)
jpg = roundtrip(im_rgb, save_all=True)
assert "mp" not in jpg.info

View File

@ -1,10 +1,17 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image
from .helper import assert_image_equal
def test_load_raw() -> None:
with Image.open("Tests/images/hopper.pcd") as im:
assert im.size == (768, 512)
im.load() # should not segfault.
# Note that this image was created with a resized hopper
@ -15,3 +22,18 @@ def test_load_raw() -> None:
# target = hopper().resize((768,512))
# assert_image_similar(im, target, 10)
@pytest.mark.parametrize("orientation", (1, 3))
def test_rotated(orientation: int) -> None:
with open("Tests/images/hopper.pcd", "rb") as fp:
data = bytearray(fp.read())
data[2048 + 1538] = orientation
f = BytesIO(data)
with Image.open(f) as im:
assert im.size == (512, 768)
with Image.open("Tests/images/hopper.pcd") as expected:
assert_image_equal(
im, expected.rotate(90 if orientation == 1 else 270, expand=True)
)

View File

@ -229,7 +229,9 @@ class TestFilePng:
assert_image(im, "RGBA", (162, 150))
# image has 124 unique alpha values
assert len(im.getchannel("A").getcolors()) == 124
colors = im.getchannel("A").getcolors()
assert colors is not None
assert len(colors) == 124
def test_load_transparent_rgb(self) -> None:
test_file = "Tests/images/rgb_trns.png"
@ -241,7 +243,9 @@ class TestFilePng:
assert_image(im, "RGBA", (64, 64))
# image has 876 transparent pixels
assert im.getchannel("A").getcolors()[0][0] == 876
colors = im.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == 876
def test_save_p_transparent_palette(self, tmp_path: Path) -> None:
in_file = "Tests/images/pil123p.png"
@ -262,7 +266,9 @@ class TestFilePng:
assert_image(im, "RGBA", (162, 150))
# image has 124 unique alpha values
assert len(im.getchannel("A").getcolors()) == 124
colors = im.getchannel("A").getcolors()
assert colors is not None
assert len(colors) == 124
def test_save_p_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/p_trns_single.png"
@ -285,7 +291,9 @@ class TestFilePng:
assert im.getpixel((31, 31)) == (0, 255, 52, 0)
# image has 876 transparent pixels
assert im.getchannel("A").getcolors()[0][0] == 876
colors = im.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == 876
def test_save_p_transparent_black(self, tmp_path: Path) -> None:
# check if solid black image with full transparency
@ -313,7 +321,9 @@ class TestFilePng:
assert im.info["transparency"] == 255
im_rgba = im.convert("RGBA")
assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
colors = im_rgba.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == num_transparent
test_file = tmp_path / "temp.png"
im.save(test_file)
@ -324,7 +334,18 @@ class TestFilePng:
assert_image_equal(im, test_im)
test_im_rgba = test_im.convert("RGBA")
assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
colors = test_im_rgba.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == num_transparent
def test_save_1_transparency(self, tmp_path: Path) -> None:
out = tmp_path / "temp.png"
im = Image.new("1", (1, 1), 1)
im.save(out, transparency=1)
with Image.open(out) as reloaded:
assert reloaded.info["transparency"] == 255
def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
in_file = "Tests/images/caption_6_33_22.png"
@ -671,6 +692,9 @@ class TestFilePng:
im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
assert reloaded.png is not None
assert reloaded.png.im_palette is not None
assert len(reloaded.png.im_palette[1]) == 48
def test_plte_length(self, tmp_path: Path) -> None:
@ -681,6 +705,9 @@ class TestFilePng:
im.save(out)
with Image.open(out) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
assert reloaded.png is not None
assert reloaded.png.im_palette is not None
assert len(reloaded.png.im_palette[1]) == 3
def test_getxmp(self) -> None:
@ -702,13 +729,17 @@ class TestFilePng:
def test_exif(self) -> None:
# With an EXIF chunk
with Image.open("Tests/images/exif.png") as im:
exif = im._getexif()
assert exif[274] == 1
assert isinstance(im, PngImagePlugin.PngImageFile)
exif_data = im._getexif()
assert exif_data is not None
assert exif_data[274] == 1
# With an ImageMagick zTXt chunk
with Image.open("Tests/images/exif_imagemagick.png") as im:
exif = im._getexif()
assert exif[274] == 1
assert isinstance(im, PngImagePlugin.PngImageFile)
exif_data = im._getexif()
assert exif_data is not None
assert exif_data[274] == 1
# Assert that info still can be extracted
# when the image is no longer a PngImageFile instance
@ -717,8 +748,10 @@ class TestFilePng:
# With a tEXt chunk
with Image.open("Tests/images/exif_text.png") as im:
exif = im._getexif()
assert exif[274] == 1
assert isinstance(im, PngImagePlugin.PngImageFile)
exif_data = im._getexif()
assert exif_data is not None
assert exif_data[274] == 1
# With XMP tags
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
@ -740,8 +773,10 @@ class TestFilePng:
im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded:
exif = reloaded._getexif()
assert exif[274] == 1
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
exif_data = reloaded._getexif()
assert exif_data is not None
assert exif_data[274] == 1
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"

View File

@ -92,6 +92,13 @@ def test_16bit_pgm() -> None:
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff")
def test_p4_save(tmp_path: Path) -> None:
with Image.open("Tests/images/hopper_1bit.pbm") as im:
filename = tmp_path / "temp.pbm"
im.save(filename)
assert_image_equal_tofile(im, filename)
def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im:
filename = tmp_path / "temp.pgm"
@ -134,6 +141,12 @@ def test_pfm_big_endian(tmp_path: Path) -> None:
assert_image_equal_tofile(im, filename)
def test_save_unsupported_mode(tmp_path: Path) -> None:
im = hopper("P")
with pytest.raises(OSError, match="cannot write mode P as PPM"):
im.save(tmp_path / "out.ppm")
@pytest.mark.parametrize(
"data",
[

View File

@ -84,8 +84,8 @@ def test_rgbx() -> None:
with Image.open(io.BytesIO(data)) as im:
r, g, b = im.split()
im = Image.merge("RGB", (b, g, r))
assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
im_rgb = Image.merge("RGB", (b, g, r))
assert_image_equal_tofile(im_rgb, os.path.join(EXTRA_DIR, "32bpp.png"))
@pytest.mark.skipif(

View File

@ -274,13 +274,17 @@ def test_save_l_transparency(tmp_path: Path) -> None:
in_file = "Tests/images/la.tga"
with Image.open(in_file) as im:
assert im.mode == "LA"
assert im.getchannel("A").getcolors()[0][0] == num_transparent
colors = im.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == num_transparent
out = tmp_path / "temp.tga"
im.save(out)
with Image.open(out) as test_im:
assert test_im.mode == "LA"
assert test_im.getchannel("A").getcolors()[0][0] == num_transparent
colors = test_im.getchannel("A").getcolors()
assert colors is not None
assert colors[0][0] == num_transparent
assert_image_equal(im, test_im)

View File

@ -764,9 +764,9 @@ class TestFileTiff:
# Test appending images
mp = BytesIO()
im = Image.new("RGB", (100, 100), "#f00")
im_rgb = Image.new("RGB", (100, 100), "#f00")
ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]]
im.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
im_rgb.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
@ -778,7 +778,7 @@ class TestFileTiff:
yield from ims
mp = BytesIO()
im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
im_rgb.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:

View File

@ -175,13 +175,13 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
del info[278]
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
im = im.resize((500, 500))
info[TiffImagePlugin.IMAGEWIDTH] = im.width
im_resized = im.resize((500, 500))
info[TiffImagePlugin.IMAGEWIDTH] = im_resized.width
# STRIPBYTECOUNTS can be a SHORT or a LONG
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
im.save(out, tiffinfo=info)
im_resized.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from io import BytesIO
from PIL import WalImageFile
from .helper import assert_image_equal_tofile
@ -13,12 +15,22 @@ def test_open() -> None:
assert im.format_description == "Quake2 Texture"
assert im.mode == "P"
assert im.size == (128, 128)
assert "next_name" not in im.info
assert isinstance(im, WalImageFile.WalImageFile)
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
def test_next_name() -> None:
with open(TEST_FILE, "rb") as fp:
data = bytearray(fp.read())
data[56:60] = b"Test"
f = BytesIO(data)
with WalImageFile.open(f) as im:
assert im.info["next_name"] == b"Test"
def test_load() -> None:
with WalImageFile.open(TEST_FILE) as im:
px = im.load()

View File

@ -4,13 +4,13 @@ from collections.abc import Generator
from pathlib import Path
import pytest
from packaging.version import parse as parse_version
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
from PIL import GifImagePlugin, Image, WebPImagePlugin
from .helper import (
assert_image_equal,
assert_image_similar,
has_feature_version,
is_big_endian,
skip_unless_feature,
)
@ -53,11 +53,8 @@ def test_write_animation_L(tmp_path: Path) -> None:
im.load()
assert_image_similar(im, orig.convert("RGBA"), 32.9)
if is_big_endian():
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")
if is_big_endian() and not has_feature_version("webp", "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)
orig.load()
@ -81,11 +78,8 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
assert_image_equal(im, frame1.convert("RGBA"))
# Compare second frame to original
if is_big_endian():
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")
if is_big_endian() and not has_feature_version("webp", "1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
im.seek(1)
im.load()
assert_image_equal(im, frame2.convert("RGBA"))

View File

@ -22,11 +22,13 @@ except ImportError:
def test_read_exif_metadata() -> None:
file_path = "Tests/images/flower.webp"
with Image.open(file_path) as image:
assert isinstance(image, WebPImagePlugin.WebPImageFile)
assert image.format == "WEBP"
exif_data = image.info.get("exif", None)
assert exif_data
exif = image._getexif()
assert exif is not None
# Camera make
assert exif[271] == "Canon"

View File

@ -44,6 +44,18 @@ def test_load_zero_inch() -> None:
pass
def test_load_unsupported_wmf() -> None:
b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x01" * 10)
with pytest.raises(SyntaxError, match="Unsupported WMF file format"):
WmfImagePlugin.WmfStubImageFile(b)
def test_load_unsupported() -> None:
b = BytesIO(b"\x01\x00\x00\x00")
with pytest.raises(SyntaxError, match="Unsupported file format"):
WmfImagePlugin.WmfStubImageFile(b)
def test_render() -> None:
with open("Tests/images/drawing.emf", "rb") as fp:
data = fp.read()

View File

@ -9,7 +9,8 @@ from .helper import skip_unless_feature
class TestFontCrash:
def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
# from fuzzers.fuzz_font
# Copy of the code from fuzz_font() in Tests/oss-fuzz/fuzzers.py
# that triggered a problem when fuzzing
font.getbbox("ABC")
font.getmask("test text")
with Image.new(mode="RGBA", size=(200, 200)) as im:

View File

@ -2,12 +2,15 @@ from __future__ import annotations
import colorsys
import itertools
from typing import Callable
from PIL import Image
from .helper import assert_image_similar, hopper
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
def int_to_float(i: int) -> float:
return i / 255

View File

@ -19,6 +19,7 @@ from PIL import (
ImageDraw,
ImageFile,
ImagePalette,
ImageShow,
UnidentifiedImageError,
features,
)
@ -283,33 +284,6 @@ class TestImage:
assert item is not None
assert item != num
def test_expand_x(self) -> None:
# Arrange
im = hopper()
orig_size = im.size
xmargin = 5
# Act
im = im._expand(xmargin)
# Assert
assert im.size[0] == orig_size[0] + 2 * xmargin
assert im.size[1] == orig_size[1] + 2 * xmargin
def test_expand_xy(self) -> None:
# Arrange
im = hopper()
orig_size = im.size
xmargin = 5
ymargin = 3
# Act
im = im._expand(xmargin, ymargin)
# Assert
assert im.size[0] == orig_size[0] + 2 * xmargin
assert im.size[1] == orig_size[1] + 2 * ymargin
def test_getbands(self) -> None:
# Assert
assert hopper("RGB").getbands() == ("R", "G", "B")
@ -388,6 +362,37 @@ class TestImage:
assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_composite_la(self) -> None:
# Arrange
expected_colors = sorted(
[
(3300, (255, 255)),
(1156, (170, 192)),
(1122, (128, 255)),
(1089, (0, 0)),
(1122, (255, 128)),
(1122, (0, 128)),
(1089, (0, 255)),
]
)
dst = Image.new("LA", size=(100, 100), color=(0, 255))
draw = ImageDraw.Draw(dst)
draw.rectangle((0, 33, 100, 66), fill=(0, 128))
draw.rectangle((0, 67, 100, 100), fill=(0, 0))
src = Image.new("LA", size=(100, 100), color=(255, 255))
draw = ImageDraw.Draw(src)
draw.rectangle((33, 0, 66, 100), fill=(255, 128))
draw.rectangle((67, 0, 100, 100), fill=(255, 0))
# Act
img = Image.alpha_composite(dst, src)
# Assert
img_colors = img.getcolors()
assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue")
@ -608,8 +613,8 @@ class TestImage:
assert im.getpixel((0, 0)) == 0
assert im.getpixel((255, 255)) == 255
with Image.open(target_file) as target:
target = target.convert(mode)
assert_image_equal(im, target)
im_target = target.convert(mode)
assert_image_equal(im, im_target)
def test_radial_gradient_wrong_mode(self) -> None:
# Arrange
@ -633,8 +638,8 @@ class TestImage:
assert im.getpixel((0, 0)) == 255
assert im.getpixel((128, 128)) == 0
with Image.open(target_file) as target:
target = target.convert(mode)
assert_image_equal(im, target)
im_target = target.convert(mode)
assert_image_equal(im, im_target)
def test_register_extensions(self) -> None:
test_format = "a"
@ -658,20 +663,20 @@ class TestImage:
assert_image_equal(im, im.remap_palette(list(range(256))))
# Test identity transform with an RGBA palette
im = Image.new("P", (256, 1))
im_p = Image.new("P", (256, 1))
for x in range(256):
im.putpixel((x, 0), x)
im.putpalette(list(range(256)) * 4, "RGBA")
im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im, im_remapped)
assert im.palette is not None
im_p.putpixel((x, 0), x)
im_p.putpalette(list(range(256)) * 4, "RGBA")
im_remapped = im_p.remap_palette(list(range(256)))
assert_image_equal(im_p, im_remapped)
assert im_p.palette is not None
assert im_remapped.palette is not None
assert im.palette.palette == im_remapped.palette.palette
assert im_p.palette.palette == im_remapped.palette.palette
# Test illegal image mode
with hopper() as im:
with hopper() as im_hopper:
with pytest.raises(ValueError):
im.remap_palette([])
im_hopper.remap_palette([])
def test_remap_palette_transparency(self) -> None:
im = Image.new("P", (1, 2), (0, 0, 0))
@ -922,6 +927,17 @@ class TestImage:
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
def test_delete_ifd_tag(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
exif.get_ifd(0x8769)
assert 0x8769 in exif
del exif[0x8769]
reloaded_exif = Image.Exif()
reloaded_exif.load(exif.tobytes())
assert 0x8769 not in reloaded_exif
def test_exif_load_from_fp(self) -> None:
with Image.open("Tests/images/flower.jpg") as im:
data = im.info["exif"]
@ -1005,6 +1021,13 @@ class TestImage:
with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"):
assert im.get_child_images() == []
def test_show(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageShow, "_viewers", [])
im = Image.new("RGB", (1, 1))
with pytest.warns(DeprecationWarning, match="Image._show"):
Image._show(im)
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
im = Image.new("RGB", size)
@ -1076,6 +1099,12 @@ class TestImage:
assert im.palette is not None
assert im.palette.colors[(27, 35, 6, 214)] == 24
def test_merge_pa(self) -> None:
p = hopper("P")
a = Image.new("L", p.size)
pa = Image.merge("PA", (p, a))
assert p.getpalette() == pa.getpalette()
def test_constants(self) -> None:
for enum in (
Image.Transpose,

View File

@ -315,3 +315,10 @@ int main(int argc, char* argv[])
process = subprocess.Popen(["embed_pil.exe"], env=env)
process.communicate()
assert process.returncode == 0
def teardown_method(self) -> None:
try:
os.remove("embed_pil.c")
except FileNotFoundError:
# If the test was skipped or failed, the file won't exist
pass

View File

@ -101,9 +101,8 @@ def test_fromarray_strides_without_tobytes() -> None:
self.__array_interface__ = arr_params
with pytest.raises(ValueError):
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)})
with pytest.warns(DeprecationWarning, match="'mode' parameter"):
Image.fromarray(wrapped, "L")
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1), "typestr": "|u1"})
Image.fromarray(wrapped, "L")
def test_fromarray_palette() -> None:
@ -112,9 +111,16 @@ def test_fromarray_palette() -> None:
a = numpy.array(i)
# Act
with pytest.warns(DeprecationWarning, match="'mode' parameter"):
out = Image.fromarray(a, "P")
out = Image.fromarray(a, "P")
# Assert that the Python and C palettes match
assert out.palette is not None
assert len(out.palette.colors) == len(out.im.getpalette()) / 3
def test_deprecation() -> None:
a = numpy.array(im.convert("L"))
with pytest.warns(
DeprecationWarning, match="'mode' parameter for changing data types"
):
Image.fromarray(a, "1")

View File

@ -80,8 +80,8 @@ def test_16bit() -> None:
_test_float_conversion(im)
for color in (65535, 65536):
im = Image.new("I", (1, 1), color)
im_i16 = im.convert("I;16")
im_i = Image.new("I", (1, 1), color)
im_i16 = im_i.convert("I;16")
assert im_i16.getpixel((0, 0)) == 65535
@ -97,6 +97,13 @@ def test_opaque() -> None:
assert_image_equal(alpha, solid)
def test_rgba() -> None:
with Image.open("Tests/images/transparent.png") as im:
assert im.mode == "RGBA"
assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5)
def test_rgba_p() -> None:
im = hopper("RGBA")
im.putalpha(hopper("L"))
@ -107,11 +114,19 @@ def test_rgba_p() -> None:
assert_image_similar(im, comparable, 20)
def test_rgba() -> None:
with Image.open("Tests/images/transparent.png") as im:
assert im.mode == "RGBA"
def test_rgba_pa() -> None:
im = hopper("RGBA").convert("PA").convert("RGB")
expected = hopper("RGB")
assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5)
assert_image_similar(im, expected, 9.3)
def test_pa() -> None:
im = hopper().convert("PA")
palette = im.palette
assert palette is not None
assert palette.colors != {}
def test_trns_p(tmp_path: Path) -> None:

View File

@ -78,13 +78,13 @@ def test_crop_crash() -> None:
extents = (1, 1, 10, 10)
# works prepatch
with Image.open(test_img) as img:
img2 = img.crop(extents)
img2.load()
img1 = img.crop(extents)
img1.load()
# fail prepatch
with Image.open(test_img) as img:
img = img.crop(extents)
img.load()
img2 = img.crop(extents)
img2.load()
def test_crop_zero() -> None:

View File

@ -15,7 +15,7 @@ def test_sanity() -> None:
def test_mode() -> None:
def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]:
def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]:
im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
data = im.getdata()
return data[0], len(data), len(list(data))

View File

@ -10,9 +10,12 @@ def test_histogram() -> None:
assert histogram("1") == (256, 0, 10994)
assert histogram("L") == (256, 0, 662)
assert histogram("LA") == (512, 0, 16384)
assert histogram("La") == (512, 0, 16384)
assert histogram("I") == (256, 0, 662)
assert histogram("F") == (256, 0, 662)
assert histogram("P") == (256, 0, 1551)
assert histogram("PA") == (512, 0, 16384)
assert histogram("RGB") == (768, 4, 675)
assert histogram("RGBA") == (1024, 0, 16384)
assert histogram("CMYK") == (1024, 0, 16384)

View File

@ -124,6 +124,21 @@ class TestImagingPaste:
im = im.crop((12, 23, im2.width + 12, im2.height + 23))
assert_image_equal(im, im2)
@pytest.mark.parametrize("y", [10, -10])
@pytest.mark.parametrize("mode", ["L", "RGB"])
@pytest.mark.parametrize("mask_mode", ["", "1", "L", "LA", "RGBa"])
def test_image_self(self, y: int, mode: str, mask_mode: str) -> None:
im = getattr(self, "gradient_" + mode)
mask = Image.new(mask_mode, im.size, 0xFFFFFFFF) if mask_mode else None
im_self = im.copy()
im_self.paste(im_self, (0, y), mask)
im_copy = im.copy()
im_copy.paste(im_copy.copy(), (0, y), mask)
assert_image_equal(im_self, im_copy)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_1(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white")

View File

@ -62,6 +62,7 @@ def test_putpalette_with_alpha_values() -> None:
expected = im.convert("RGBA")
palette = im.getpalette()
assert palette is not None
transparency = im.info.pop("transparency")
palette_with_alpha_values = []

View File

@ -1,11 +1,16 @@
from __future__ import annotations
import pytest
from packaging.version import parse as parse_version
from PIL import Image, features
from PIL import Image
from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature
from .helper import (
assert_image_similar,
has_feature_version,
hopper,
is_ppc64le,
skip_unless_feature,
)
def test_sanity() -> None:
@ -23,11 +28,8 @@ def test_sanity() -> None:
@skip_unless_feature("libimagequant")
def test_libimagequant_quantize() -> None:
image = hopper()
if is_ppc64le():
version = features.version_feature("libimagequant")
assert version is not None
if parse_version(version) < parse_version("4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
if is_ppc64le() and not has_feature_version("libimagequant", "4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15)
@ -56,8 +58,8 @@ def test_rgba_quantize() -> None:
def test_quantize() -> None:
with Image.open("Tests/images/caption_6_33_22.png") as image:
image = image.convert("RGB")
converted = image.quantize()
converted = image.convert("RGB")
converted = converted.quantize()
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 1)
@ -65,13 +67,13 @@ def test_quantize() -> None:
def test_quantize_no_dither() -> None:
image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette:
palette = palette.convert("P")
palette_p = palette.convert("P")
converted = image.quantize(dither=Image.Dither.NONE, palette=palette)
converted = image.quantize(dither=Image.Dither.NONE, palette=palette_p)
assert converted.mode == "P"
assert converted.palette is not None
assert palette.palette is not None
assert converted.palette.palette == palette.palette.palette
assert palette_p.palette is not None
assert converted.palette.palette == palette_p.palette.palette
def test_quantize_no_dither2() -> None:
@ -95,10 +97,10 @@ def test_quantize_no_dither2() -> None:
def test_quantize_dither_diff() -> None:
image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette:
palette = palette.convert("P")
palette_p = palette.convert("P")
dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette)
nodither = image.quantize(dither=Image.Dither.NONE, palette=palette)
dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette_p)
nodither = image.quantize(dither=Image.Dither.NONE, palette=palette_p)
assert dither.tobytes() != nodither.tobytes()
@ -116,6 +118,15 @@ def test_quantize_kmeans(method: Image.Quantize) -> None:
im.quantize(kmeans=-1, method=method)
@skip_unless_feature("libimagequant")
def test_resize() -> None:
im = hopper().resize((100, 100))
converted = im.quantize(100, Image.Quantize.LIBIMAGEQUANT)
colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_colors() -> None:
im = hopper()
colors = 2

View File

@ -314,8 +314,8 @@ class TestImageResize:
@skip_unless_feature("libtiff")
def test_transposed(self) -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im:
im = im.resize((64, 64))
assert im.size == (64, 64)
im_resized = im.resize((64, 64))
assert im_resized.size == (64, 64)
@pytest.mark.parametrize(
"mode", ("L", "RGB", "I", "I;16", "I;16L", "I;16B", "I;16N", "F")

View File

@ -43,8 +43,8 @@ def test_angle(angle: int) -> None:
with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle)
im = hopper()
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
im_hopper = hopper()
assert_image_equal(im_hopper.rotate(angle), im_hopper.rotate(angle, expand=1))
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
@ -76,9 +76,9 @@ def test_center_0() -> None:
with Image.open("Tests/images/hopper_45.png") as target:
target_origin = target.size[1] / 2
target = target.crop((0, target_origin, 128, target_origin + 128))
im_target = target.crop((0, target_origin, 128, target_origin + 128))
assert_image_similar(im, target, 15)
assert_image_similar(im, im_target, 15)
def test_center_14() -> None:
@ -87,22 +87,22 @@ def test_center_14() -> None:
with Image.open("Tests/images/hopper_45.png") as target:
target_origin = target.size[1] / 2 - 14
target = target.crop((6, target_origin, 128 + 6, target_origin + 128))
im_target = target.crop((6, target_origin, 128 + 6, target_origin + 128))
assert_image_similar(im, target, 10)
assert_image_similar(im, im_target, 10)
def test_translate() -> None:
im = hopper()
with Image.open("Tests/images/hopper_45.png") as target:
target_origin = (target.size[1] / 2 - 64) - 5
target = target.crop(
im_target = target.crop(
(target_origin, target_origin, target_origin + 128, target_origin + 128)
)
im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC)
assert_image_similar(im, target, 1)
assert_image_similar(im, im_target, 1)
def test_fastpath_center() -> None:

View File

@ -159,9 +159,9 @@ def test_reducing_gap_for_DCT_scaling() -> None:
with Image.open("Tests/images/hopper.jpg") as ref:
# thumbnail should call draft with reducing_gap scale
ref.draft(None, (18 * 3, 18 * 3))
ref = ref.resize((18, 18), Image.Resampling.BICUBIC)
im_ref = ref.resize((18, 18), Image.Resampling.BICUBIC)
with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
assert_image_similar(ref, im, 1.4)
assert_image_similar(im_ref, im, 1.4)

View File

@ -1,7 +1,6 @@
from __future__ import annotations
import math
from typing import Callable
import pytest
@ -9,6 +8,10 @@ from PIL import Image, ImageTransform
from .helper import assert_image_equal, assert_image_similar, hopper
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
class TestImageTransform:
def test_sanity(self) -> None:

View File

@ -1,11 +1,13 @@
from __future__ import annotations
from typing import Callable
from PIL import Image, ImageChops
from .helper import assert_image_equal, hopper
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable
BLACK = (0, 0, 0)
BROWN = (127, 64, 0)
CYAN = (0, 255, 255)

View File

@ -7,7 +7,7 @@ import shutil
import sys
from io import BytesIO
from pathlib import Path
from typing import Any, Literal, cast
from typing import Literal, cast
import pytest
@ -31,6 +31,9 @@ except ImportError:
# Skipped via setup_module()
pass
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any
SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc"
HAVE_PROFILE = os.path.exists(SRGB)
@ -208,9 +211,10 @@ def test_exceptions() -> None:
ImageCms.getProfileName(None) # type: ignore[arg-type]
skip_missing()
# Python <= 3.9: "an integer is required (got type NoneType)"
# Python > 3.9: "'NoneType' object cannot be interpreted as an integer"
with pytest.raises(ImageCms.PyCMSError, match="integer"):
with pytest.raises(
ImageCms.PyCMSError,
match="'NoneType' object cannot be interpreted as an integer",
):
ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type]
@ -690,3 +694,17 @@ def test_cmyk_lab() -> None:
im = Image.new("CMYK", (1, 1))
converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (255, 128, 128)
def test_deprecation() -> None:
profile = ImageCmsProfile(ImageCms.createProfile("sRGB"))
with pytest.warns(
DeprecationWarning, match="ImageCms.ImageCmsProfile.product_name"
):
profile.product_name
with pytest.warns(
DeprecationWarning, match="ImageCms.ImageCmsProfile.product_info"
):
profile.product_info
with pytest.raises(AttributeError):
profile.this_attribute_does_not_exist

View File

@ -1,13 +1,10 @@
from __future__ import annotations
import os.path
from collections.abc import Sequence
from typing import Callable
import pytest
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
from PIL._typing import Coords
from .helper import (
assert_image_equal,
@ -17,6 +14,12 @@ from .helper import (
skip_unless_feature,
)
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from PIL._typing import Coords
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (190, 190, 190)
@ -195,10 +198,10 @@ def test_bitmap() -> None:
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
with Image.open("Tests/images/pil123rgba.png") as small:
small = small.resize((50, 50), Image.Resampling.NEAREST)
small_resized = small.resize((50, 50), Image.Resampling.NEAREST)
# Act
draw.bitmap((10, 10), small)
draw.bitmap((10, 10), small_resized)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png")
@ -1491,7 +1494,9 @@ def test_default_font_size() -> None:
def draw_text() -> None:
draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
assert_image_similar_tofile(
im, "Tests/images/imagedraw_default_font_size.png", 1
)
check(draw_text)
@ -1510,7 +1515,9 @@ def test_default_font_size() -> None:
def draw_multiline_text() -> None:
draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
assert_image_similar_tofile(
im, "Tests/images/imagedraw_default_font_size.png", 1
)
check(draw_multiline_text)

View File

@ -164,6 +164,11 @@ class TestImageFile:
with pytest.raises(OSError):
p.close()
def test_negative_offset(self) -> None:
with Image.open("Tests/images/raw_negative_stride.bin") as im:
with pytest.raises(ValueError, match="Tile offset cannot be negative"):
im.load()
def test_no_format(self) -> None:
buf = BytesIO(b"\x00" * 255)

View File

@ -19,6 +19,7 @@ from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar_tofile,
has_feature_version,
is_win32,
skip_unless_feature,
skip_unless_feature_version,
@ -492,6 +493,11 @@ def test_stroke_mask() -> None:
assert mask.getpixel((42, 5)) == 255
def test_load_invalid_file() -> None:
with pytest.raises(SyntaxError, match="Not a PILfont file"):
ImageFont.load("Tests/images/1_trns.png")
def test_load_when_image_not_found() -> None:
with tempfile.NamedTemporaryFile(delete=False) as tmp:
pass
@ -549,7 +555,7 @@ def test_default_font() -> None:
draw.text((10, 60), txt, font=larger_default_font)
# Assert
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
assert_image_similar_tofile(im, "Tests/images/default_font_freetype.png", 0.13)
@pytest.mark.parametrize("mode", ("", "1", "RGBA"))
@ -1055,7 +1061,10 @@ def test_colr(layout_engine: ImageFont.Layout) -> None:
d.text((15, 5), "Bungee", font=font, embedded_color=True)
assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21)
if has_feature_version("freetype2", "2.14.0"):
assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 6.1)
else:
assert_image_similar_tofile(im, "Tests/images/colr_bungee_older.png", 21)
@skip_unless_feature_version("freetype2", "2.10.0")
@ -1071,7 +1080,7 @@ def test_colr_mask(layout_engine: ImageFont.Layout) -> None:
d.text((15, 5), "Bungee", "black", font=font)
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 14.1)
def test_woff2(layout_engine: ImageFont.Layout) -> None:

View File

@ -7,6 +7,7 @@ from PIL import Image, ImageDraw, ImageFont
from .helper import (
assert_image_equal_tofile,
assert_image_similar_tofile,
has_feature_version,
skip_unless_feature,
)
@ -104,11 +105,9 @@ def test_text_direction_ttb() -> None:
im = Image.new(mode="RGB", size=(100, 300))
draw = ImageDraw.Draw(im)
try:
draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb")
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
pytest.skip("libraqm 0.7 or greater not available")
if not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb")
target = "Tests/images/test_direction_ttb.png"
assert_image_similar_tofile(im, target, 2.8)
@ -119,19 +118,17 @@ def test_text_direction_ttb_stroke() -> None:
im = Image.new(mode="RGB", size=(100, 300))
draw = ImageDraw.Draw(im)
try:
draw.text(
(27, 27),
"あい",
font=ttf,
fill=500,
direction="ttb",
stroke_width=2,
stroke_fill="#0f0",
)
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
pytest.skip("libraqm 0.7 or greater not available")
if not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
draw.text(
(27, 27),
"あい",
font=ttf,
fill=500,
direction="ttb",
stroke_width=2,
stroke_fill="#0f0",
)
target = "Tests/images/test_direction_ttb_stroke.png"
assert_image_similar_tofile(im, target, 19.4)
@ -186,7 +183,7 @@ def test_x_max_and_y_offset() -> None:
draw.text((0, 0), "لح", font=ttf, fill=500)
target = "Tests/images/test_x_max_and_y_offset.png"
assert_image_similar_tofile(im, target, 0.5)
assert_image_similar_tofile(im, target, 3.8)
def test_language() -> None:
@ -219,14 +216,9 @@ def test_getlength(
im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im)
try:
assert d.textlength(text, ttf, direction) == expected
except ValueError as ex:
if (
direction == "ttb"
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
):
pytest.skip("libraqm 0.7 or greater not available")
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
assert d.textlength(text, ttf, direction) == expected
@pytest.mark.parametrize("mode", ("L", "1"))
@ -242,17 +234,12 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None:
ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
try:
target = ttf.getlength("ii", mode, direction)
actual = ttf.getlength(text, mode, direction)
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
target = ttf.getlength("ii", mode, direction)
actual = ttf.getlength(text, mode, direction)
assert actual == target
except ValueError as ex:
if (
direction == "ttb"
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
):
pytest.skip("libraqm 0.7 or greater not available")
assert actual == target
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
@ -265,11 +252,9 @@ def test_anchor_ttb(anchor: str) -> None:
d = ImageDraw.Draw(im)
d.line(((0, 200), (200, 200)), "gray")
d.line(((100, 0), (100, 400)), "gray")
try:
d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
pytest.skip("libraqm 0.7 or greater not available")
if not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
assert_image_similar_tofile(im, path, 1) # fails at 5
@ -310,10 +295,12 @@ combine_tests = (
# this tests various combining characters for anchor alignment and clipping
@pytest.mark.parametrize(
"name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests]
"name, text, anchor, direction, epsilon",
combine_tests,
ids=[r[0] for r in combine_tests],
)
def test_combine(
name: str, text: str, dir: str | None, anchor: str | None, epsilon: float
name: str, text: str, direction: str | None, anchor: str | None, epsilon: float
) -> None:
path = f"Tests/images/test_combine_{name}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
@ -322,11 +309,9 @@ def test_combine(
d = ImageDraw.Draw(im)
d.line(((0, 200), (400, 200)), "gray")
d.line(((200, 0), (200, 400)), "gray")
try:
d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f)
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
pytest.skip("libraqm 0.7 or greater not available")
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
pytest.skip("libraqm 0.7 or greater not available")
d.text((200, 200), text, fill="black", anchor=anchor, direction=direction, font=f)
assert_image_similar_tofile(im, path, epsilon)

View File

@ -30,6 +30,14 @@ def test_default_font(font: ImageFont.ImageFont) -> None:
assert_image_equal_tofile(im, "Tests/images/default_font.png")
def test_invalid_mode() -> None:
font = ImageFont.ImageFont()
fp = BytesIO()
with Image.open("Tests/images/hopper.png") as im:
with pytest.raises(TypeError, match="invalid font image mode"):
font._load_pilfont_data(fp, im)
def test_without_freetype() -> None:
original_core = ImageFont.core
if features.check_module("freetype2"):

View File

@ -2,7 +2,9 @@ from __future__ import annotations
from typing import Any
from PIL import Image, ImageMath
import pytest
from PIL import Image, ImageMath, _imagingmath
def pixel(im: Image.Image | int) -> str | int:
@ -498,3 +500,31 @@ def test_logical_not_equal() -> None:
)
== "I 1"
)
def test_reflected_operands() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: 1 + args["A"], **images)) == "I 2"
assert pixel(ImageMath.lambda_eval(lambda args: 1 - args["A"], **images)) == "I 0"
assert pixel(ImageMath.lambda_eval(lambda args: 1 * args["A"], **images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: 1 / args["A"], **images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: 1 % args["A"], **images)) == "I 0"
assert pixel(ImageMath.lambda_eval(lambda args: 1 ** args["A"], **images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: 1 & args["A"], **images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: 1 | args["A"], **images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: 1 ^ args["A"], **images)) == "I 0"
def test_unsupported_mode() -> None:
im = Image.new("RGB", (1, 1))
with pytest.raises(ValueError, match="unsupported mode: RGB"):
ImageMath.lambda_eval(lambda args: args["im"] + 1, im=im)
def test_bad_operand_type(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delattr(_imagingmath, "abs_I")
with pytest.raises(TypeError, match="bad operand type for 'abs'"):
ImageMath.lambda_eval(lambda args: abs(args["I"]), I=I)
monkeypatch.delattr(_imagingmath, "max_F")
with pytest.raises(TypeError, match="bad operand type for 'max'"):
ImageMath.lambda_eval(lambda args: args["max"](args["I"], args["F"]), I=I, F=F)

View File

@ -7,7 +7,7 @@ import pytest
from PIL import Image, ImageMorph, _imagingmorph
from .helper import assert_image_equal_tofile, hopper
from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower_valgrind
def string_to_img(image_string: str) -> Image.Image:
@ -266,16 +266,18 @@ def test_unknown_pattern() -> None:
ImageMorph.LutBuilder(op_name="unknown")
def test_pattern_syntax_error() -> None:
@pytest.mark.parametrize(
"pattern", ("a pattern with a syntax error", "4:(" + "X" * 30000)
)
@timeout_unless_slower_valgrind(1)
def test_pattern_syntax_error(pattern: str) -> None:
# Arrange
lb = ImageMorph.LutBuilder(op_name="corner")
new_patterns = ["a pattern with a syntax error"]
new_patterns = [pattern]
lb.add_patterns(new_patterns)
# Act / Assert
with pytest.raises(
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
):
with pytest.raises(Exception, match='Syntax error in pattern "'):
lb.build_lut()

View File

@ -186,6 +186,21 @@ def test_palette(mode: str) -> None:
)
def test_rgba_palette() -> None:
im = Image.new("P", (1, 1))
red = (255, 0, 0, 255)
translucent_black = (0, 0, 0, 127)
im.putpalette(red + translucent_black, "RGBA")
expanded_im = ImageOps.expand(im, 1, 1)
palette = expanded_im.palette
assert palette is not None
assert palette.mode == "RGBA"
assert expanded_im.convert("RGBA").getpixel((0, 0)) == translucent_black
def test_pil163() -> None:
# Division by zero in equalize if < 255 pixels in image (@PIL163)
@ -246,10 +261,10 @@ def test_colorize_2color() -> None:
# Open test image (256px by 10px, black to white)
with Image.open("Tests/images/bw_gradient.png") as im:
im = im.convert("L")
im_l = im.convert("L")
# Create image with original 2-color functionality
im_test = ImageOps.colorize(im, "red", "green")
im_test = ImageOps.colorize(im_l, "red", "green")
# Test output image (2-color)
left = (0, 1)
@ -286,11 +301,11 @@ def test_colorize_2color_offset() -> None:
# Open test image (256px by 10px, black to white)
with Image.open("Tests/images/bw_gradient.png") as im:
im = im.convert("L")
im_l = im.convert("L")
# Create image with original 2-color functionality with offsets
im_test = ImageOps.colorize(
im, black="red", white="green", blackpoint=50, whitepoint=100
im_l, black="red", white="green", blackpoint=50, whitepoint=100
)
# Test output image (2-color) with offsets
@ -328,11 +343,11 @@ def test_colorize_3color_offset() -> None:
# Open test image (256px by 10px, black to white)
with Image.open("Tests/images/bw_gradient.png") as im:
im = im.convert("L")
im_l = im.convert("L")
# Create image with new three color functionality with offsets
im_test = ImageOps.colorize(
im,
im_l,
black="red",
white="green",
mid="blue",

View File

@ -49,6 +49,12 @@ def test_getcolor() -> None:
palette.getcolor("unknown") # type: ignore[arg-type]
def test_getcolor_rgba() -> None:
palette = ImagePalette.ImagePalette("RGBA", (1, 2, 3, 4))
palette.getcolor((5, 6, 7, 8))
assert palette.palette == b"\x01\x02\x03\x04\x05\x06\x07\x08"
def test_getcolor_rgba_color_rgb_palette() -> None:
palette = ImagePalette.ImagePalette("RGB")

View File

@ -76,9 +76,14 @@ def test_consecutive() -> None:
def test_palette_mmap() -> None:
# Using mmap in ImageFile can require to reload the palette.
with Image.open("Tests/images/multipage-mmap.tiff") as im:
color1 = im.getpalette()[:3]
palette = im.getpalette()
assert palette is not None
color1 = palette[:3]
im.seek(0)
color2 = im.getpalette()[:3]
palette = im.getpalette()
assert palette is not None
color2 = palette[:3]
assert color1 == color2

View File

@ -59,15 +59,12 @@ def test_show(mode: str) -> None:
assert ImageShow.show(im)
def test_show_without_viewers() -> None:
viewers = ImageShow._viewers
ImageShow._viewers = []
def test_show_without_viewers(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageShow, "_viewers", [])
with hopper() as im:
assert not ImageShow.show(im)
ImageShow._viewers = viewers
@pytest.mark.parametrize(
"viewer",

View File

@ -57,3 +57,13 @@ def test_constant() -> None:
assert st.rms[0] == 128
assert st.var[0] == 0
assert st.stddev[0] == 0
def test_zero_count() -> None:
im = Image.new("L", (0, 0))
st = ImageStat.Stat(im)
assert st.mean == [0]
assert st.rms == [0]
assert st.var == [0]

110
Tests/test_imagetext.py Normal file
View File

@ -0,0 +1,110 @@
from __future__ import annotations
import pytest
from PIL import Image, ImageDraw, ImageFont, ImageText, features
from .helper import assert_image_similar_tofile, skip_unless_feature
FONT_PATH = "Tests/fonts/FreeMono.ttf"
@pytest.fixture(
scope="module",
params=[
pytest.param(ImageFont.Layout.BASIC),
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
],
)
def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
return request.param
@pytest.fixture(
scope="module",
params=[
None,
pytest.param(ImageFont.Layout.BASIC, marks=skip_unless_feature("freetype2")),
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
],
)
def font(
request: pytest.FixtureRequest,
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont:
layout_engine = request.param
if layout_engine is None:
return ImageFont.load_default_imagefont()
else:
return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine)
def test_get_length(font: ImageFont.ImageFont | ImageFont.FreeTypeFont) -> None:
factor = 1 if isinstance(font, ImageFont.ImageFont) else 2
assert ImageText.Text("A", font).get_length() == 6 * factor
assert ImageText.Text("AB", font).get_length() == 12 * factor
assert ImageText.Text("M", font).get_length() == 6 * factor
assert ImageText.Text("y", font).get_length() == 6 * factor
assert ImageText.Text("a", font).get_length() == 6 * factor
text = ImageText.Text("\n", font)
with pytest.raises(ValueError, match="can't measure length of multiline text"):
text.get_length()
@pytest.mark.parametrize(
"text, expected",
(
("A", (0, 4, 12, 16)),
("AB", (0, 4, 24, 16)),
("M", (0, 4, 12, 16)),
("y", (0, 7, 12, 20)),
("a", (0, 7, 12, 16)),
),
)
def test_get_bbox(
font: ImageFont.ImageFont | ImageFont.FreeTypeFont,
text: str,
expected: tuple[int, int, int, int],
) -> None:
if isinstance(font, ImageFont.ImageFont):
expected = (0, 0, expected[2] // 2, 11)
assert ImageText.Text(text, font).get_bbox() == expected
def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None:
if features.check_module("freetype2"):
font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
text = ImageText.Text("Hello World!", font)
text.embed_color()
assert text.get_length() == 288
im = Image.new("RGB", (300, 64), "white")
draw = ImageDraw.Draw(im)
draw.text((10, 10), text, "#fa6")
assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1)
text = ImageText.Text("", mode="1")
with pytest.raises(
ValueError, match="Embedded color supported only in RGB and RGBA modes"
):
text.embed_color()
@skip_unless_feature("freetype2")
def test_stroke() -> None:
for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items():
# Arrange
im = Image.new("RGB", (120, 130))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype(FONT_PATH, 120)
text = ImageText.Text("A", font)
text.stroke(2, stroke_fill)
# Act
draw.text((12, 12), text, "#f00")
# Assert
assert_image_similar_tofile(
im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1
)

302
Tests/test_nanoarrow.py Normal file
View File

@ -0,0 +1,302 @@
from __future__ import annotations
import json
from typing import Any, NamedTuple
import pytest
from PIL import Image
from .helper import (
assert_deep_equal,
assert_image_equal,
hopper,
is_big_endian,
)
TYPE_CHECKING = False
if TYPE_CHECKING:
import nanoarrow # type: ignore [import-not-found]
else:
nanoarrow = pytest.importorskip("nanoarrow", reason="Nanoarrow not installed")
TEST_IMAGE_SIZE = (10, 10)
def _test_img_equals_pyarray(
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
) -> None:
assert img.height * img.width * elts_per_pixel == len(arr)
px = img.load()
assert px is not None
if elts_per_pixel > 1 and mask is None:
# have to do element-wise comparison when we're comparing
# flattened r,g,b,a to a pixel.
mask = list(range(elts_per_pixel))
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
if mask:
pixel = px[x, y]
assert isinstance(pixel, tuple)
for ix, elt in enumerate(mask):
if elts_per_pixel == 1:
assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
else:
assert (
pixel[ix]
== arr[(y * img.width + x) * elts_per_pixel + elt].as_py()
)
else:
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
def _test_img_equals_int32_pyarray(
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
) -> None:
assert img.height * img.width * elts_per_pixel == len(arr)
px = img.load()
assert px is not None
if mask is None:
# have to do element-wise comparison when we're comparing
# flattened rgba in an uint32 to a pixel.
mask = list(range(elts_per_pixel))
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
pixel = px[x, y]
assert isinstance(pixel, tuple)
arr_pixel_int = arr[y * img.width + x].as_py()
arr_pixel_tuple = (
arr_pixel_int % 256,
(arr_pixel_int // 256) % 256,
(arr_pixel_int // 256**2) % 256,
(arr_pixel_int // 256**3),
)
if is_big_endian():
arr_pixel_tuple = arr_pixel_tuple[::-1]
for ix, elt in enumerate(mask):
assert pixel[ix] == arr_pixel_tuple[elt]
fl_uint8_4_type = nanoarrow.fixed_size_list(
value_type=nanoarrow.uint8(nullable=False), list_size=4, nullable=False
)
@pytest.mark.parametrize(
"mode, dtype, mask",
(
("L", nanoarrow.uint8(nullable=False), None),
("I", nanoarrow.int32(nullable=False), None),
("F", nanoarrow.float32(nullable=False), None),
("LA", fl_uint8_4_type, [0, 3]),
("RGB", fl_uint8_4_type, [0, 1, 2]),
("RGBA", fl_uint8_4_type, None),
("RGBX", fl_uint8_4_type, None),
("CMYK", fl_uint8_4_type, None),
("YCbCr", fl_uint8_4_type, [0, 1, 2]),
("HSV", fl_uint8_4_type, [0, 1, 2]),
),
)
def test_to_array(mode: str, dtype: nanoarrow, mask: list[int] | None) -> None:
img = hopper(mode)
# Resize to non-square
img = img.crop((3, 0, 124, 127))
assert img.size == (121, 127)
arr = nanoarrow.Array(img)
_test_img_equals_pyarray(img, arr, mask)
assert arr.schema.type == dtype.type
assert arr.schema.nullable == dtype.nullable
reloaded = Image.fromarrow(arr, mode, img.size)
assert_image_equal(img, reloaded)
def test_lifetime() -> None:
# valgrind shouldn't error out here.
# arrays should be accessible after the image is deleted.
img = hopper("L")
arr_1 = nanoarrow.Array(img)
arr_2 = nanoarrow.Array(img)
del img
assert sum(arr_1.iter_py()) > 0
del arr_1
assert sum(arr_2.iter_py()) > 0
del arr_2
def test_lifetime2() -> None:
# valgrind shouldn't error out here.
# img should remain after the arrays are collected.
img = hopper("L")
arr_1 = nanoarrow.Array(img)
arr_2 = nanoarrow.Array(img)
assert sum(arr_1.iter_py()) > 0
del arr_1
assert sum(arr_2.iter_py()) > 0
del arr_2
img2 = img.copy()
px = img2.load()
assert px # make mypy happy
assert isinstance(px[0, 0], int)
class DataShape(NamedTuple):
dtype: nanoarrow
# Strictly speaking, elt should be a pixel or pixel component, so
# list[uint8][4], float, int, uint32, uint8, etc. But more
# correctly, it should be exactly the dtype from the line above.
elt: Any
elts_per_pixel: int
UINT_ARR = DataShape(
dtype=fl_uint8_4_type,
elt=[1, 2, 3, 4], # array of 4 uint8 per pixel
elts_per_pixel=1, # only one array per pixel
)
UINT = DataShape(
dtype=nanoarrow.uint8(),
elt=3, # one uint8,
elts_per_pixel=4, # but repeated 4x per pixel
)
UINT32 = DataShape(
dtype=nanoarrow.uint32(),
elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000
elts_per_pixel=1, # one per pixel
)
INT32 = DataShape(
dtype=nanoarrow.uint32(),
elt=0x12CDEF45, # one packed int
elts_per_pixel=1, # one per pixel
)
@pytest.mark.parametrize(
"mode, data_tp, mask",
(
("L", DataShape(nanoarrow.uint8(), 3, 1), None),
("I", DataShape(nanoarrow.int32(), 1 << 24, 1), None),
("F", DataShape(nanoarrow.float32(), 3.14159, 1), None),
("LA", UINT_ARR, [0, 3]),
("LA", UINT, [0, 3]),
("RGB", UINT_ARR, [0, 1, 2]),
("RGBA", UINT_ARR, None),
("CMYK", UINT_ARR, None),
("YCbCr", UINT_ARR, [0, 1, 2]),
("HSV", UINT_ARR, [0, 1, 2]),
("RGB", UINT, [0, 1, 2]),
("RGBA", UINT, None),
("CMYK", UINT, None),
("YCbCr", UINT, [0, 1, 2]),
("HSV", UINT, [0, 1, 2]),
),
)
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
if dtype == fl_uint8_4_type:
tmp_arr = nanoarrow.Array(
elt * (ct_pixels * elts_per_pixel), schema=nanoarrow.uint8()
)
c_array = nanoarrow.c_array_from_buffers(
dtype, ct_pixels, buffers=[], children=[tmp_arr]
)
arr = nanoarrow.Array(c_array)
else:
arr = nanoarrow.Array(
nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype)
)
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
_test_img_equals_pyarray(img, arr, mask, elts_per_pixel)
@pytest.mark.parametrize(
"mode, mask",
(
("LA", [0, 3]),
("RGB", [0, 1, 2]),
("RGBA", None),
("CMYK", None),
("YCbCr", [0, 1, 2]),
("HSV", [0, 1, 2]),
),
)
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
(dtype, elt, elts_per_pixel) = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = nanoarrow.Array(
nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype)
)
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
_test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)
@pytest.mark.parametrize(
"mode, metadata",
(
("LA", ["L", "X", "X", "A"]),
("RGB", ["R", "G", "B", "X"]),
("RGBX", ["R", "G", "B", "X"]),
("RGBA", ["R", "G", "B", "A"]),
("CMYK", ["C", "M", "Y", "K"]),
("YCbCr", ["Y", "Cb", "Cr", "X"]),
("HSV", ["H", "S", "V", "X"]),
),
)
def test_image_nested_metadata(mode: str, metadata: list[str]) -> None:
img = hopper(mode)
arr = nanoarrow.Array(img)
assert arr.schema.value_type.metadata
assert arr.schema.value_type.metadata[b"image"]
parsed_metadata = json.loads(
arr.schema.value_type.metadata[b"image"].decode("utf8")
)
assert "bands" in parsed_metadata
assert parsed_metadata["bands"] == metadata
@pytest.mark.parametrize(
"mode, metadata",
(
("L", ["L"]),
("I", ["I"]),
("F", ["F"]),
),
)
def test_image_flat_metadata(mode: str, metadata: list[str]) -> None:
img = hopper(mode)
arr = nanoarrow.Array(img)
assert arr.schema.metadata
assert arr.schema.metadata[b"image"]
parsed_metadata = json.loads(arr.schema.metadata[b"image"].decode("utf8"))
assert "bands" in parsed_metadata
assert parsed_metadata["bands"] == metadata

View File

@ -28,15 +28,13 @@ def test_numpy_to_image() -> None:
a = numpy.array(data, dtype=dtype)
a.shape = TEST_IMAGE_SIZE
i = Image.fromarray(a)
if list(i.getdata()) != data:
print("data mismatch for", dtype)
assert list(i.getdata()) == data
else:
data = list(range(100))
a = numpy.array([[x] * bands for x in data], dtype=dtype)
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
i = Image.fromarray(a)
if list(i.getchannel(0).getdata()) != list(range(100)):
print("data mismatch for", dtype)
assert list(i.getchannel(0).getdata()) == list(range(100))
return i
# Check supported 1-bit integer formats

View File

@ -90,18 +90,18 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange
filename = tmp_path / "temp.pkl"
with Image.open("Tests/images/hopper.jpg") as im:
im = im.convert("PA")
im_pa = im.convert("PA")
# Act / Assert
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
im._mode = "LA"
im_pa._mode = "LA"
with open(filename, "wb") as f:
pickle.dump(im, f, protocol)
pickle.dump(im_pa, f, protocol)
with open(filename, "rb") as f:
loaded_im = pickle.load(f)
im._mode = "PA"
assert im == loaded_im
im_pa._mode = "PA"
assert im_pa == loaded_im
@skip_unless_feature("webp")

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import json
from typing import Any, NamedTuple
import pytest
@ -244,3 +245,29 @@ def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None)
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
_test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)
@pytest.mark.parametrize(
"mode, metadata",
(
("LA", ["L", "X", "X", "A"]),
("RGB", ["R", "G", "B", "X"]),
("RGBX", ["R", "G", "B", "X"]),
("RGBA", ["R", "G", "B", "A"]),
("CMYK", ["C", "M", "Y", "K"]),
("YCbCr", ["Y", "Cb", "Cr", "X"]),
("HSV", ["H", "S", "V", "X"]),
),
)
def test_image_metadata(mode: str, metadata: list[str]) -> None:
img = hopper(mode)
arr = pyarrow.array(img) # type: ignore[call-overload]
assert arr.type.field(0).metadata
assert arr.type.field(0).metadata[b"image"]
parsed_metadata = json.loads(arr.type.field(0).metadata[b"image"].decode("utf8"))
assert "bands" in parsed_metadata
assert parsed_metadata["bands"] == metadata

View File

@ -9,9 +9,30 @@ from PIL import __version__
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
def map_metadata_keys(md):
# Convert installed wheel metadata into canonical Core Metadata 2.4 format.
# This was a utility method in pyroma 4.3.3; it was removed in 5.0.
# This implementation is constructed from the relevant logic from
# Pyroma 5.0's `build_metadata()` implementation. This has been submitted
# upstream to Pyroma as https://github.com/regebro/pyroma/pull/116,
# so it may be possible to simplify this test in future.
data = {}
for key in set(md.keys()):
value = md.get_all(key)
key = pyroma.projectdata.normalize(key)
if len(value) == 1:
value = value[0]
if value.strip() == "UNKNOWN":
continue
data[key] = value
return data
def test_pyroma() -> None:
# Arrange
data = pyroma.projectdata.map_metadata_keys(metadata("Pillow"))
data = map_metadata_keys(metadata("Pillow"))
# Act
rating = pyroma.ratings.rate(data)

View File

@ -1,8 +1,5 @@
from __future__ import annotations
from pathlib import Path
from typing import Union
import pytest
from PIL import Image, ImageQt
@ -11,18 +8,8 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper
TYPE_CHECKING = False
if TYPE_CHECKING:
import PyQt6
import PySide6
from pathlib import Path
QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication]
QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel]
QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint]
QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion]
QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget]
if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap
@ -32,11 +19,16 @@ if ImageQt.qt_is_installed:
from PyQt6.QtGui import QImage, QPainter, QRegion
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
elif ImageQt.qt_version == "side6":
from PySide6.QtCore import QPoint
from PySide6.QtGui import QImage, QPainter, QRegion
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
from PySide6.QtCore import QPoint # type: ignore[assignment]
from PySide6.QtGui import QImage, QPainter, QRegion # type: ignore[assignment]
from PySide6.QtWidgets import ( # type: ignore[assignment]
QApplication,
QHBoxLayout,
QLabel,
QWidget,
)
class Example(QWidget): # type: ignore[misc]
class Example(QWidget):
def __init__(self) -> None:
super().__init__()
@ -47,9 +39,9 @@ if ImageQt.qt_is_installed:
pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage)
# hbox
QHBoxLayout(self) # type: ignore[operator]
QHBoxLayout(self)
lbl = QLabel(self) # type: ignore[operator]
lbl = QLabel(self)
# Segfault in the problem
lbl.setPixmap(pixmap1.copy())
@ -63,7 +55,7 @@ def roundtrip(expected: Image.Image) -> None:
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_sanity(tmp_path: Path) -> None:
# Segfault test
app: QApplication | None = QApplication([]) # type: ignore[operator]
app: QApplication | None = QApplication([])
ex = Example()
assert app # Silence warning
assert ex # Silence warning
@ -84,11 +76,11 @@ def test_sanity(tmp_path: Path) -> None:
imageqt = ImageQt.ImageQt(im)
data = getattr(QPixmap, "fromImage")(imageqt)
qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator]
painter = QPainter(qimage) # type: ignore[operator]
image_label = QLabel() # type: ignore[operator]
qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32"))
painter = QPainter(qimage)
image_label = QLabel()
image_label.setPixmap(data)
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator]
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128))
painter.end()
rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png")
qimage.save(rendered_tempfile)

View File

@ -1,13 +1,15 @@
from __future__ import annotations
from pathlib import Path
import pytest
from PIL import ImageQt
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
TYPE_CHECKING = False
if TYPE_CHECKING:
from pathlib import Path
pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
)
@ -21,7 +23,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode)
data = ImageQt.toqimage(src)
assert isinstance(data, QImage) # type: ignore[arg-type, misc]
assert isinstance(data, QImage)
assert not data.isNull()
# reload directly from the qimage

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